prax-orm 0.11.0

A next-generation, type-safe ORM for Rust inspired by Prisma
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
---
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 (&#64;)</h4>
            <ul class="text-muted text-sm space-y-1">
              <li>• <code>&#64;id</code> - Primary key</li>
              <li>• <code>&#64;auto</code> - Auto-increment</li>
              <li>• <code>&#64;unique</code> - Unique constraint</li>
              <li>• <code>&#64;default()</code> - Default value</li>
              <li>• <code>&#64;map()</code> - Column name mapping</li>
              <li>• <code>&#64;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 (&#64;&#64;)</h4>
            <ul class="text-muted text-sm space-y-1">
              <li>• <code>&#64;&#64;map()</code> - Table name mapping</li>
              <li>• <code>&#64;&#64;id([])</code> - Composite primary key</li>
              <li>• <code>&#64;&#64;unique([])</code> - Composite unique</li>
              <li>• <code>&#64;&#64;index([])</code> - Create index</li>
            </ul>
            <p class="text-muted text-xs mt-3">
              Note: <code>&#64;&#64;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">&#64;&#64;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>&#64;&#64;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>&#64;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">&#64;&#64;map("name")</code></td>
                <td class="py-3 px-4">Custom table name in database</td>
                <td class="py-3 px-4"><code>&#64;&#64;map("app_users")</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;id([fields])</code></td>
                <td class="py-3 px-4">Composite primary key</td>
                <td class="py-3 px-4"><code>&#64;&#64;id([tenantId, id])</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;unique([fields])</code></td>
                <td class="py-3 px-4">Composite unique constraint</td>
                <td class="py-3 px-4"><code>&#64;&#64;unique([email, tenantId])</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;index([fields])</code></td>
                <td class="py-3 px-4">Create index on fields</td>
                <td class="py-3 px-4"><code>&#64;&#64;index([status, createdAt])</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;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">&#64;&#64;ignore</code></td>
                <td class="py-3 px-4">Exclude from client generation</td>
                <td class="py-3 px-4"><code>&#64;&#64;ignore</code></td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>
    </div>
  </article>
</DocsLayout>