# How pgrust Manages User-Defined Types and Domains: A Deep Dive into the Type Cache System

> Discover how pgrust manages user-defined types and domains using a centralized type cache system and specialized modules. Learn about constraint validation and I/O operations.

- Repository: [Michael Malis/pgrust](https://github.com/malisper/pgrust)
- Tags: deep-dive
- Published: 2026-07-13

---

**pgrust represents all PostgreSQL user-defined types—including composite, enum, range, and domain types—through a centralized type-cache abstraction (`TypeCacheEntry`) that resolves metadata via the typcache seam crate, while domain-specific constraint validation and I/O operations are handled in a dedicated module that wraps base type functions with constraint checking.**

The `malisper/pgrust` project reimplements PostgreSQL's core server logic in Rust, including a complete type system for handling user-defined objects. Understanding how pgrust manages user-defined types and domains requires examining the typcache subsystem and the specialized domain handling routines that enforce constraints while delegating to underlying base types.

## The Type Cache Architecture

At the heart of pgrust's type system lies the **typcache** subsystem, which eliminates repeated catalog lookups by caching metadata for every user-defined type.

### TypeCacheEntry and Metadata Resolution

When the engine encounters a user-defined type, it invokes `typcache_seams::lookup_type_cache` to retrieve a `TypeCacheEntry` structure. This cache entry contains critical metadata including the type OID, base type references for domains, associated operators, and function pointers. According to the `backend-utils-cache-typcache` seam crate implementation, this abstraction decouples the core logic from direct PostgreSQL catalog access while preserving semantic compatibility.

### Composite Types and TupleDesc

For user-defined composite types (row types), the typcache provides a `TupleDesc` (tuple descriptor) that describes the field structure. The function `lookup_rowtype_tupdesc` returns this descriptor when given a composite type OID. This same mechanism applies to **domain-over-composite** types, where the domain wrapper preserves the underlying composite structure while adding constraints.

## Domain Types: Constraints and Validation

Domain types in pgrust wrap existing base types—whether built-in or user-defined—and layer `NOT NULL` and `CHECK` constraints on top. The implementation lives in [`crates/backend/utils/adt/misc2/src/domains.rs`](https://github.com/malisper/pgrust/blob/main/crates/backend/utils/adt/misc2/src/domains.rs), which provides four distinct entry points: `domain_in` and `domain_recv` for input conversion, plus `domain_check` and `domain_check_safe` for validation.

### Base Type Resolution and I/O Functions

Domain input processing begins with `domain_get_base_input_info`, which queries the typcache to obtain the underlying type's I/O functions. The flow proceeds as follows:

1. **Base type lookup** – The system calls `typcache_seams::domain_get_base_input_info` to retrieve the base type's input function OID and type information.
2. **Input conversion** – The engine invokes `fmgr_seams::input_function_call` (or `oid_receive_function_call` for binary input) to convert the raw input string into a `Datum` using the base type's logic.

This delegation ensures that domains inherit all base type parsing behavior while remaining positioned to validate the result.

### Constraint Checking (NOT NULL and CHECK)

After obtaining the base value, pgrust validates it against domain constraints through `typcache_seams::domain_check_input`. This function checks:

- **NOT NULL constraints** – Rejecting null values for non-nullable domains
- **CHECK constraints** – Evaluating domain-specific expression constraints

The validation occurs in [`crates/backend/utils/adt/misc2/src/domains.rs`](https://github.com/malisper/pgrust/blob/main/crates/backend/utils/adt/misc2/src/domains.rs), where the `domain_check_input` seam bridges to the constraint enforcement logic.

### Hard vs. Soft Error Handling

pgrust distinguishes between **hard errors** (fatal exceptions) and **soft errors** (captured diagnostics) through the `SoftErrorContext` mechanism:

- **Hard error path** – Used by `domain_recv` and standard `domain_in` calls, allowing malformed input to raise a fatal `PgError` that aborts the operation.
- **Soft error path** – Available via `domain_check_safe`, which accepts a mutable `SoftErrorContext` reference and returns `Ok(Datum::null())` for constraint violations instead of panicking.

This dual-mode handling supports both strict SQL semantics and incremental validation scenarios.

## Working with Domains in Practice

The following examples demonstrate how to interact with user-defined domain types in pgrust code.

### Reading Text Input into a Domain

This example converts a text string into a domain type using hard error semantics:

```rust
use mcx::Mcx;
use types_error::PgResult;
use types_tuple::heaptuple::Datum;

/// Read a text value into a user-defined domain (e.g., positive_int)
fn read_domain_value(mcx: Mcx<'_>, txt: &str, domain_oid: u32) -> PgResult<Datum<'_>> {
    // Hard error on invalid input - no soft error context provided
    pgrust::crates::backend::utils::adt::misc2::domains::domain_in(
        mcx,
        Some(txt),
        domain_oid,
        -1,  // typmod
        None, // No soft error sink
    )
}

```

### Soft Validation of Existing Datums

This example validates an existing datum against domain constraints without raising fatal errors:

```rust
use mcx::Mcx;
use types_error::{PgResult, SoftErrorContext};
use types_tuple::heaptuple::Datum;

/// Validate a datum against domain constraints, capturing errors softly
fn validate_domain_soft(
    mcx: Mcx<'_>,
    datum: &Datum<'_>,
    is_null: bool,
    domain_oid: u32,
) -> PgResult<bool> {
    let mut soft_ctx = SoftErrorContext::default();
    pgrust::crates::backend::utils::adt::misc2::domains::domain_check_safe(
        mcx,
        datum,
        is_null,
        domain_oid,
        Some(&mut soft_ctx),
    )
}

```

## Summary

- **Type caching** – pgrust uses `TypeCacheEntry` structures via the `typcache_seams` crate to cache metadata for all user-defined types, eliminating repeated catalog lookups.
- **Domain delegation** – Domain input functions in [`crates/backend/utils/adt/misc2/src/domains.rs`](https://github.com/malisper/pgrust/blob/main/crates/backend/utils/adt/misc2/src/domains.rs) delegate to base type I/O functions through `fmgr_seams`, then apply constraint checks.
- **Constraint enforcement** – `domain_check_input` validates `NOT NULL` and `CHECK` constraints, supporting both hard-error and soft-error execution modes.
- **Composite support** – `lookup_rowtype_tupdesc` provides `TupleDesc` structures for composite types and domain-over-composite types.
- **Seam architecture** – The implementation relies on seam crates (`typcache_seams`, `fmgr_seams`) to maintain pure-Rust code while preserving PostgreSQL semantics.

## Frequently Asked Questions

### How does pgrust handle user-defined composite types?

pgrust handles composite types through the typcache subsystem by calling `lookup_rowtype_tupdesc`, which returns a `TupleDesc` containing field names and types. This descriptor is cached in the `TypeCacheEntry` for the composite type OID, allowing efficient repeated access to the row structure without re-parsing system catalogs.

### What is the difference between hard and soft error contexts in domain validation?

Hard error contexts (used by `domain_in` and `domain_recv`) immediately raise a `PgError` when constraint validation fails, aborting the current operation. Soft error contexts (used by `domain_check_safe`) capture validation failures in a `SoftErrorContext` structure and return a null datum, allowing the caller to handle constraint violations programmatically without transaction abort.

### Where does pgrust store metadata for user-defined types?

Metadata resides in the **typcache** (type cache), implemented in the `backend-utils-cache-typcache` seam crate. The `lookup_type_cache` function retrieves `TypeCacheEntry` structures containing OIDs, base type references, operator information, and constraint details for domains, all cached to avoid repeated system catalog queries.

### How does domain type inheritance work in pgrust?

Domains inherit their underlying base type's I/O behavior through delegation. When processing domain input, `domain_get_base_input_info` retrieves the base type's input function from the typcache, and `input_function_call` executes that function. After obtaining the base value, pgrust applies domain-specific constraints, effectively layering behavioral restrictions on top of the inherited type's characteristics.