---
import DocsLayout from '../../layouts/DocsLayout.astro';
import CodeBlock from '../../components/CodeBlock.astro';
const basicModel = `// A model represents a database table
model User {
id Int @id @auto // Primary key with auto-increment
email String @unique // Unique constraint
name String? // Optional (nullable) field
}`;
const modelAnatomy = `model ModelName {
// ┌─ Field name (camelCase convention)
// │ ┌─ Field type (scalar, enum, or relation)
// │ │ ┌─ Type modifier (? = optional, [] = array)
// │ │ │ ┌─ Attributes (start with @)
// │ │ │ │
fieldName FieldType? @attribute(args)
// Model-level attributes start with @@
@@modelAttribute([fields])
}`;
const fullModel = `/// User account in the system
/// Stores authentication and profile information
model User {
// Primary key
id Int @id @auto
// Unique identifiers
email String @unique
username String @unique @validate.minLength(3)
// Profile information
name String?
bio String? @db.Text
avatarUrl String? @map("avatar_url")
// Authentication
/// @writeonly - Not returned in responses (doc-comment metadata, not an attribute)
passwordHash String @map("password_hash")
// Status and role
role Role @default(USER)
status Status @default(ACTIVE)
emailVerified Boolean @default(false) @map("email_verified")
// Relations
posts Post[]
comments Comment[]
profile Profile?
sessions Session[]
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Model attributes
@@map("users") // Table name
@@index([email]) // Performance index
@@index([createdAt]) // For sorting
@@unique([username, deletedAt]) // Soft-delete aware unique
}`;
const compositeKeys = `// Composite primary key
model PostTag {
post Post @relation(fields: [postId], references: [id])
postId Int
tag Tag @relation(fields: [tagId], references: [id])
tagId Int
@@id([postId, tagId]) // Composite primary key
}
// Composite primary key with additional fields
model Membership {
user User @relation(fields: [userId], references: [id])
userId Int
org Org @relation(fields: [orgId], references: [id])
orgId Int
role String @default("member")
joinedAt DateTime @default(now())
@@id([userId, orgId]) // User can only be in org once
}
// Multi-tenant composite key
model TenantUser {
tenantId Int
id Int @default(autoincrement())
email String
name String?
@@id([tenantId, id]) // Tenant-scoped ID
@@unique([tenantId, email]) // Email unique per tenant
@@map("tenant_users")
}`;
const indexes = `model Product {
id Int @id @auto
name String
sku String @unique
price Decimal
category String
subcategory String
brand String?
inStock Boolean @default(true)
createdAt DateTime @default(now())
// Single-field indexes
@@index([name]) // Basic index
@@index([createdAt]) // For sorting
// Composite indexes (for multi-column queries)
@@index([category, subcategory]) // Category filtering
@@index([category, price]) // Category + price range
@@index([brand, inStock]) // Brand with stock filter
// Named index
@@index([name, category], name: "product_search_idx")
// Unique composite constraint
@@unique([category, sku])
// Hash index (equality queries only, PostgreSQL)
@@index([sku], type: Hash)
// GIN index for full-text search (PostgreSQL)
@@index([name, description], type: GIN)
// Partial/filtered index (PostgreSQL)
@@index([price], where: "in_stock = true", name: "active_products_price")
}`;
const vectorIndexes = `// Vector indexes for AI/ML embeddings (requires pgvector extension)
// Database URL is configured in prax.toml, not in the schema
datasource db {
provider = "postgresql"
extensions = [vector] // Enable pgvector extension
}
model Document {
id Int @id @auto
title String
content String
embedding Vector(1536) // OpenAI text-embedding-ada-002 dimension
// HNSW index - better recall, faster queries, slower builds
@@index([embedding], type: Hnsw, ops: Cosine)
}
model Image {
id Int @id @auto
filename String
embedding Vector(512) // CLIP embedding dimension
// IVFFlat index - faster builds, good for large datasets
@@index([embedding], type: IvfFlat, ops: L2, lists: 100)
}
// Vector index options:
// - type: Hnsw | IvfFlat
// - ops: Cosine | L2 | InnerProduct
// - m: HNSW max connections (default 16)
// - ef_construction: HNSW build quality (default 64)
// - lists: IVFFlat inverted lists (default 100)
model SemanticSearch {
id Int @id @auto
content String
dense Vector(768) // Dense embedding (BERT)
sparse SparseVector(30000) // Sparse embedding (SPLADE)
binary Bit(256) // Binary quantized vector
// HNSW with custom parameters
@@index([dense], type: Hnsw, ops: Cosine, m: 32, ef_construction: 128)
// Inner product for max similarity search
@@index([dense], type: Hnsw, ops: InnerProduct, name: "semantic_ip_idx")
}`;
const softDelete = `// Soft delete pattern
model Document {
id Int @id @auto
title String
content String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete marker
// Index for efficient queries excluding deleted
@@index([deletedAt])
// Unique constraint that allows duplicates if deleted
@@unique([title, deletedAt])
}
// Multi-tenant with soft delete
model TenantDocument {
id Int @id @auto
tenantId Int
title String
deletedAt DateTime?
// Unique title per tenant (active documents only)
@@unique([tenantId, title, deletedAt])
@@index([tenantId, deletedAt])
}`;
const multiTenant = `// Row-level multi-tenancy
model TenantAwareModel {
id Int @id @auto
tenantId Int // Tenant discriminator
name String
@@index([tenantId]) // Fast tenant filtering
@@unique([tenantId, name]) // Unique per tenant
}
// Schema- and database-level isolation are configured at runtime through
// prax-query's tenant module (TenantConfig::row_level / schema_based /
// database_based) — NOT via schema attributes. @@schema(...) and
// @@datasource(...) are not implemented. See the Multi-Tenancy guide:
// /advanced/multitenancy`;
const documentation = `/// User account for authentication and profile management
///
/// This model stores user credentials and profile information.
/// Soft deletes are supported via the deletedAt field.
///
/// @since 1.0.0
/// @see Profile for extended profile information
/// @see Post for user's content
model User {
/// Unique identifier, auto-generated
/// @internal Used for foreign keys
id Int @id @auto
/// User's email address
/// @example "john@example.com"
/// @validation Must be a valid email format
email String @unique @validate.email
/// Display name
/// @nullable
/// @maxLength 100
name String? @validate.maxLength(100)
/// @deprecated Use 'role' enum instead
/// @since 0.1.0
/// @until 2.0.0
isAdmin Boolean @default(false)
}`;
const naming = `// ✅ Good naming conventions
model User { } // Singular PascalCase
model BlogPost { } // Multi-word PascalCase
model APIKey { } // Acronyms in caps
model Example {
id Int @id // Lowercase field names
firstName String // camelCase for multi-word
createdAt DateTime // Common timestamp names
userId Int // Foreign key: modelId
@@map("examples") // Lowercase plural table name
}
// ❌ Avoid these patterns
// model users { } // Don't use plural
// model user { } // Don't use lowercase
// model USER_TABLE { } // Don't use SCREAMING_CASE
// model UserModel { } // Don't add "Model" suffix`;
const generatedCode = `// Generated Rust code from the User model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub email: String,
pub name: Option<String>,
pub role: Role,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// Query builder module
pub mod user {
pub mod id {
pub fn equals(value: i32) -> Filter { ... }
pub fn in_(values: Vec<i32>) -> Filter { ... }
pub fn lt(value: i32) -> Filter { ... }
pub fn gt(value: i32) -> Filter { ... }
}
pub mod email {
pub fn equals(value: &str) -> Filter { ... }
pub fn contains(value: &str) -> Filter { ... }
pub fn starts_with(value: &str) -> Filter { ... }
}
// ... more fields
}
// Usage
let users = client
.user()
.find_many()
.where(user::role::equals(Role::ADMIN))
.order_by(user::created_at::desc())
.exec()
.await?;`;
---
<DocsLayout title="Models - Prax ORM">
<article class="max-w-4xl mx-auto px-6 py-12">
<header class="mb-12">
<h1 class="text-4xl font-bold mb-4">Models</h1>
<p class="text-xl text-muted">
Models are the foundation of your Prax schema, representing database tables and their structure.
</p>
</header>
<div class="space-y-12">
<!-- Introduction -->
<section>
<h2 class="text-2xl font-semibold mb-4">What is a Model?</h2>
<p class="text-muted mb-4">
A model defines a database table and its columns. Each model generates type-safe Rust code
including the struct definition, query builders, and filter functions. Models are the core
building block of your Prax schema.
</p>
<CodeBlock code={basicModel} lang="prax" filename="prax/schema.prax" />
</section>
<!-- Anatomy -->
<section>
<h2 class="text-2xl font-semibold mb-4">Model Anatomy</h2>
<p class="text-muted mb-4">
Understanding the structure of a model definition:
</p>
<CodeBlock code={modelAnatomy} lang="prax" />
<div class="mt-6 grid md:grid-cols-2 gap-4">
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-primary-400">Field Attributes (@)</h4>
<ul class="text-muted text-sm space-y-1">
<li>• <code>@id</code> - Primary key</li>
<li>• <code>@auto</code> - Auto-increment</li>
<li>• <code>@unique</code> - Unique constraint</li>
<li>• <code>@default()</code> - Default value</li>
<li>• <code>@map()</code> - Column name mapping</li>
<li>• <code>@relation()</code> - Define relations</li>
</ul>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-primary-400">Model Attributes (@@)</h4>
<ul class="text-muted text-sm space-y-1">
<li>• <code>@@map()</code> - Table name mapping</li>
<li>• <code>@@id([])</code> - Composite primary key</li>
<li>• <code>@@unique([])</code> - Composite unique</li>
<li>• <code>@@index([])</code> - Create index</li>
</ul>
<p class="text-muted text-xs mt-3">
Note: <code>@@schema()</code> is <strong>not yet supported</strong> in v0.11.
</p>
</div>
</div>
</section>
<!-- Complete Example -->
<section>
<h2 class="text-2xl font-semibold mb-4">Complete Model Example</h2>
<p class="text-muted mb-4">
Here's a production-ready User model showcasing common patterns:
</p>
<CodeBlock code={fullModel} lang="prax" filename="prax/schema.prax" />
</section>
<!-- Composite Keys -->
<section>
<h2 class="text-2xl font-semibold mb-4">Composite Primary Keys</h2>
<p class="text-muted mb-4">
Use <code class="px-2 py-1 bg-surface-elevated rounded">@@id([fields])</code> to define
composite primary keys. This is common for join tables and multi-tenant schemas.
</p>
<CodeBlock code={compositeKeys} lang="prax" filename="prax/schema.prax" />
</section>
<!-- Indexes -->
<section>
<h2 class="text-2xl font-semibold mb-4">Indexes</h2>
<p class="text-muted mb-4">
Indexes improve query performance. Prax supports various index types depending on your database.
</p>
<CodeBlock code={indexes} lang="prax" filename="prax/schema.prax" />
<div class="mt-6 overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border">
<th class="text-left py-3 px-4 font-semibold">Index Type</th>
<th class="text-left py-3 px-4 font-semibold">Use Case</th>
<th class="text-left py-3 px-4 font-semibold">Databases</th>
</tr>
</thead>
<tbody class="text-muted">
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">B-Tree</code></td>
<td class="py-3 px-4">Default, range queries, sorting</td>
<td class="py-3 px-4">All</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">Hash</code></td>
<td class="py-3 px-4">Equality comparisons only</td>
<td class="py-3 px-4">PostgreSQL, MySQL</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">GIN</code></td>
<td class="py-3 px-4">Arrays, JSONB, full-text search</td>
<td class="py-3 px-4">PostgreSQL</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">GiST</code></td>
<td class="py-3 px-4">Geometric data, full-text search</td>
<td class="py-3 px-4">PostgreSQL</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">BRIN</code></td>
<td class="py-3 px-4">Large tables with sorted data</td>
<td class="py-3 px-4">PostgreSQL</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Soft Delete -->
<section>
<h2 class="text-2xl font-semibold mb-4">Soft Delete Pattern</h2>
<p class="text-muted mb-4">
Soft deletes preserve data by marking records as deleted instead of physically removing them.
Use a nullable <code class="px-2 py-1 bg-surface-elevated rounded">deletedAt</code> timestamp field.
</p>
<CodeBlock code={softDelete} lang="prax" filename="prax/schema.prax" />
</section>
<!-- Multi-Tenancy -->
<section>
<h2 class="text-2xl font-semibold mb-4">Multi-Tenant Models</h2>
<p class="text-muted mb-4">
Prax supports various multi-tenancy patterns for SaaS applications:
</p>
<CodeBlock code={multiTenant} lang="prax" filename="prax/schema.prax" />
<div class="mt-4 p-4 rounded-xl bg-info-500/10 border border-info-500/30">
<p class="text-info-400 text-sm">
<strong>Tip:</strong> Row-level tenancy is the simplest to implement. Prax's tenant
module also supports schema-based and database-based isolation configured at runtime.
See the <a href="/advanced/multitenancy" class="underline">Multi-Tenancy documentation</a>
for details.
</p>
</div>
</section>
<!-- Documentation -->
<section>
<h2 class="text-2xl font-semibold mb-4">Documenting Models</h2>
<p class="text-muted mb-4">
Use triple-slash comments (<code class="px-2 py-1 bg-surface-elevated rounded">///</code>) to document
your models and fields. Documentation is preserved in generated code and API schemas.
</p>
<CodeBlock code={documentation} lang="prax" filename="prax/schema.prax" />
</section>
<!-- Naming Conventions -->
<section>
<h2 class="text-2xl font-semibold mb-4">Naming Conventions</h2>
<CodeBlock code={naming} lang="prax" filename="prax/schema.prax" />
<div class="mt-6 grid gap-4">
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-primary-400">Models</h4>
<p class="text-muted text-sm">
Use singular <code class="px-1 bg-surface-elevated rounded">PascalCase</code>.
The generated table name will be lowercase plural (unless overridden with <code>@@map</code>).
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-primary-400">Fields</h4>
<p class="text-muted text-sm">
Use <code class="px-1 bg-surface-elevated rounded">camelCase</code> for field names.
Use <code>@map()</code> for snake_case column names if needed.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-primary-400">Foreign Keys</h4>
<p class="text-muted text-sm">
Use <code class="px-1 bg-surface-elevated rounded">modelId</code> pattern
(e.g., <code>userId</code>, <code>postId</code>).
</p>
</div>
</div>
</section>
<!-- Generated Code -->
<section>
<h2 class="text-2xl font-semibold mb-4">Generated Rust Code</h2>
<p class="text-muted mb-4">
Models generate type-safe Rust structs and query builder modules:
</p>
<CodeBlock code={generatedCode} lang="rust" filename="src/generated/user.rs" />
</section>
<!-- Model Attributes Reference -->
<section>
<h2 class="text-2xl font-semibold mb-4">Model Attributes Reference</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border">
<th class="text-left py-3 px-4 font-semibold">Attribute</th>
<th class="text-left py-3 px-4 font-semibold">Description</th>
<th class="text-left py-3 px-4 font-semibold">Example</th>
</tr>
</thead>
<tbody class="text-muted">
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@map("name")</code></td>
<td class="py-3 px-4">Custom table name in database</td>
<td class="py-3 px-4"><code>@@map("app_users")</code></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@id([fields])</code></td>
<td class="py-3 px-4">Composite primary key</td>
<td class="py-3 px-4"><code>@@id([tenantId, id])</code></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@unique([fields])</code></td>
<td class="py-3 px-4">Composite unique constraint</td>
<td class="py-3 px-4"><code>@@unique([email, tenantId])</code></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@index([fields])</code></td>
<td class="py-3 px-4">Create index on fields</td>
<td class="py-3 px-4"><code>@@index([status, createdAt])</code></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@schema("name")</code></td>
<td class="py-3 px-4"><strong>Not yet supported</strong> — inert in the DSL, compile error in the derive macro</td>
<td class="py-3 px-4">—</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4"><code class="text-primary-400">@@ignore</code></td>
<td class="py-3 px-4">Exclude from client generation</td>
<td class="py-3 px-4"><code>@@ignore</code></td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</article>
</DocsLayout>