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
---
import DocsLayout from '../../layouts/DocsLayout.astro';
import CodeBlock from '../../components/CodeBlock.astro';

const basicView = `// Define a view based on SQL query
view UserStats {
    id          Int
    email       String
    postCount   Int      @map("post_count")
    totalLikes  Int      @map("total_likes")
    lastPostAt  DateTime? @map("last_post_at")

    @@sql("""
        SELECT
            u.id,
            u.email,
            COUNT(p.id) as post_count,
            COALESCE(SUM(p.likes), 0) as total_likes,
            MAX(p.created_at) as last_post_at
        FROM users u
        LEFT JOIN posts p ON p.author_id = u.id
        GROUP BY u.id, u.email
    """)
}`;

const simpleView = `// Simple view for filtering active users
view ActiveUser {
    id        Int
    email     String
    name      String?
    createdAt DateTime @map("created_at")

    @@sql("SELECT id, email, name, created_at FROM users WHERE active = true")
}

// View with joined data
view PostWithAuthor {
    id          Int
    title       String
    authorName  String  @map("author_name")
    authorEmail String  @map("author_email")
    publishedAt DateTime? @map("published_at")

    @@sql("""
        SELECT
            p.id,
            p.title,
            u.name as author_name,
            u.email as author_email,
            p.published_at
        FROM posts p
        JOIN users u ON u.id = p.author_id
        WHERE p.published = true
    """)
}`;

const materializedView = `// Materialized view for expensive queries (PostgreSQL)
view MonthlyRevenue {
    month       String    @id
    year        Int
    revenue     Decimal
    orderCount  Int       @map("order_count")
    avgOrder    Decimal   @map("avg_order")

    @@sql("""
        SELECT
            TO_CHAR(created_at, 'YYYY-MM') as month,
            EXTRACT(YEAR FROM created_at)::INT as year,
            SUM(total) as revenue,
            COUNT(*) as order_count,
            AVG(total) as avg_order
        FROM orders
        WHERE status = 'COMPLETED'
        GROUP BY TO_CHAR(created_at, 'YYYY-MM'),
                 EXTRACT(YEAR FROM created_at)
    """)

    @@materialized          // Create as MATERIALIZED VIEW
    @@refreshInterval("1h") // Refresh every hour (if supported)
}`;

const viewWithRelations = `// Base models
model User {
    id    Int    @id @auto
    email String @unique
    name  String?
    posts Post[]
}

model Post {
    id        Int      @id @auto
    title     String
    content   String?
    published Boolean  @default(false)
    likes     Int      @default(0)
    author    User     @relation(fields: [authorId], references: [id])
    authorId  Int      @map("author_id")
    createdAt DateTime @default(now()) @map("created_at")
}

// View aggregating user stats
view UserDashboard {
    userId       Int     @id @map("user_id")
    email        String
    name         String?
    totalPosts   Int     @map("total_posts")
    publishedCnt Int     @map("published_count")
    totalLikes   Int     @map("total_likes")
    avgLikes     Float   @map("avg_likes")

    @@sql("""
        SELECT
            u.id as user_id,
            u.email,
            u.name,
            COUNT(p.id) as total_posts,
            COUNT(p.id) FILTER (WHERE p.published) as published_count,
            COALESCE(SUM(p.likes), 0) as total_likes,
            COALESCE(AVG(p.likes), 0) as avg_likes
        FROM users u
        LEFT JOIN posts p ON p.author_id = u.id
        GROUP BY u.id, u.email, u.name
    """)
}`;

const queryingViews = `// Views are queried like models (read-only)
use prax_query::filter::Filter;
use prax_query::types::OrderByField;
use crate::generated::{UserStats, user_stats};

// Find all user stats
let stats = client
    .user_stats()
    .find_many()
    .exec()
    .await?;

// Generated view field modules expose only a COLUMN constant
// (pub const COLUMN: &str) — no gte/desc/sum helper functions.
// Build filters from the COLUMN constants or plain column-name strings:
let top_users = client
    .user_stats()
    .find_many()
    .r#where(Filter::Gte(user_stats::post_count::COLUMN.into(), 10.into()))
    .order_by(OrderByField::desc(user_stats::total_likes::COLUMN))
    .take(10)
    .exec()
    .await?;

// Find unique by ID
let user_stat = client
    .user_stats()
    .find_unique()
    .r#where(Filter::Equals(user_stats::id::COLUMN.into(), 1.into()))
    .exec()
    .await?;

// The generated module also includes a lightweight Query builder for
// column selection, ordering, and pagination only — it does not accept
// filter predicates:
let query = user_stats::Query::new()
    .select(user_stats::email::COLUMN)
    .take(10);`;

const databaseSpecific = `// PostgreSQL-specific view features
view SearchResults {
    id         Int
    title      String
    content    String
    rank       Float

    @@sql("""
        SELECT
            id,
            title,
            content,
            ts_rank(search_vector, to_tsquery('english', 'prax')) as rank
        FROM documents
        WHERE search_vector @@ to_tsquery('english', 'prax')
        ORDER BY rank DESC
    """)
}

// MySQL-specific
view RecentOrders {
    id         Int
    total      Decimal
    status     String
    customerName String @map("customer_name")

    @@sql("""
        SELECT
            o.id,
            o.total,
            o.status,
            c.name as customer_name
        FROM orders o
        JOIN customers c ON c.id = o.customer_id
        WHERE o.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
    """)
}`;

const viewBestPractices = `// ✅ Good: Descriptive view names that indicate aggregation
view UserPostStatistics { ... }
view MonthlyRevenueReport { ... }
view ActiveSubscriptions { ... }

// ✅ Good: Document complex views
/// Aggregated statistics for the admin dashboard
/// Updated: Real-time (not materialized)
/// Performance: ~200ms for 10k users
view AdminDashboardStats {
    ...
}

// ✅ Good: Use column aliases for clarity
@@sql("""
    SELECT
        user_id,          -- Clear reference
        COUNT(*) as order_count,  -- Descriptive alias
        SUM(total) as total_spent -- Meaningful name
    FROM orders
    GROUP BY user_id
""")

// ❌ Avoid: Overly complex views
// Break into smaller views or use CTEs

// ❌ Avoid: Views without indexes on base tables
// Ensure proper indexes exist for view queries`;
---

<DocsLayout title="Views - 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">Views</h1>
      <p class="text-xl text-muted">
        Define read-only database views with custom SQL for aggregations, joins, and complex queries.
      </p>
    </header>

    <div class="space-y-12">
      <!-- Introduction -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">What are Views?</h2>
        <p class="text-muted mb-4">
          Database views are virtual tables defined by SQL queries. They provide a way to encapsulate
          complex queries, aggregate data, and expose a simplified interface for reading data.
          Views in Prax are read-only and generate type-safe Rust code for querying.
        </p>
        <div class="grid md:grid-cols-2 gap-4 mb-6">
          <div class="p-4 rounded-xl bg-success-500/10 border border-success-500/30">
            <h4 class="font-semibold text-success-400 mb-2">Use Cases</h4>
            <ul class="text-muted text-sm space-y-1">
              <li>• Aggregate statistics (counts, sums, averages)</li>
              <li>• Pre-joined data for dashboards</li>
              <li>• Filtered subsets of data</li>
              <li>• Denormalized data for APIs</li>
              <li>• Full-text search results</li>
            </ul>
          </div>
          <div class="p-4 rounded-xl bg-warning-500/10 border border-warning-500/30">
            <h4 class="font-semibold text-warning-400 mb-2">Limitations</h4>
            <ul class="text-muted text-sm space-y-1">
              <li>• Read-only (no create, update, delete)</li>
              <li>• No relations to other models</li>
              <li>• Query performance depends on base tables</li>
              <li>• Materialized views need manual refresh</li>
            </ul>
          </div>
        </div>
      </section>

      <!-- Basic View -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Basic View Definition</h2>
        <p class="text-muted mb-4">
          Define a view with the <code class="px-2 py-1 bg-surface-elevated rounded">view</code> keyword,
          fields that match your SQL output, and a <code class="px-2 py-1 bg-surface-elevated rounded">&#64;&#64;sql()</code>
          attribute containing the query.
        </p>
        <CodeBlock code={basicView} lang="prax" filename="prax/schema.prax" />
      </section>

      <!-- Simple Views -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Simple Views</h2>
        <p class="text-muted mb-4">
          Views can be as simple as filtering or joining data. Use <code class="px-2 py-1 bg-surface-elevated rounded">&#64;map()</code>
          to map field names to SQL column aliases.
        </p>
        <CodeBlock code={simpleView} lang="prax" filename="prax/schema.prax" />
      </section>

      <!-- Materialized Views -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Materialized Views</h2>
        <p class="text-muted mb-4">
          For expensive queries that don't need real-time data, use materialized views. They store
          the query results and can be refreshed periodically. Supported on PostgreSQL.
        </p>
        <CodeBlock code={materializedView} 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>Note:</strong> Materialized views need to be refreshed manually or on a schedule.
            Use <code class="px-1 bg-surface-elevated rounded">REFRESH MATERIALIZED VIEW view_name</code> in PostgreSQL
            or configure automatic refresh with <code class="px-1 bg-surface-elevated rounded">&#64;&#64;refreshInterval</code>.
          </p>
        </div>
      </section>

      <!-- Views with Base Models -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Complete Example with Models</h2>
        <p class="text-muted mb-4">
          Here's a complete example showing models and a view that aggregates their data:
        </p>
        <CodeBlock code={viewWithRelations} lang="prax" filename="prax/schema.prax" />
      </section>

      <!-- Querying Views -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Querying Views</h2>
        <p class="text-muted mb-4">
          Views are queried using the same type-safe API as models. All read operations
          (find_many, find_unique, find_first, count, aggregate) are available.
        </p>
        <CodeBlock code={queryingViews} lang="rust" filename="src/main.rs" />
        <div class="mt-4 p-4 rounded-xl bg-warning-500/10 border border-warning-500/30">
          <p class="text-warning-400 text-sm">
            <strong>v0.11 breaking change:</strong> the generated view <code>Query</code> builder no
            longer accepts raw-string <code>where_conditions</code>. It supports column selection,
            ordering, and pagination only. For filtered view queries, use the typed client with
            <code>prax_query::filter::Filter</code>, which binds predicate values as parameters
            instead of interpolating raw strings into SQL.
          </p>
        </div>
      </section>

      <!-- Database-Specific Features -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Database-Specific Features</h2>
        <p class="text-muted mb-4">
          Different databases offer unique view capabilities. Prax supports database-specific SQL in your view definitions.
        </p>
        <CodeBlock code={databaseSpecific} lang="prax" filename="prax/schema.prax" />
      </section>

      <!-- View Attributes Reference -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">View 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;sql("...")</code></td>
                <td class="py-3 px-4">The SQL query that defines the view</td>
                <td class="py-3 px-4"><code>&#64;&#64;sql("SELECT * FROM users")</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;materialized</code></td>
                <td class="py-3 px-4">Create as materialized view (PostgreSQL)</td>
                <td class="py-3 px-4"><code>&#64;&#64;materialized</code></td>
              </tr>
              <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 view name in database</td>
                <td class="py-3 px-4"><code>&#64;&#64;map("user_statistics")</code></td>
              </tr>
              <tr class="border-b border-border">
                <td class="py-3 px-4"><code class="text-primary-400">&#64;&#64;refreshInterval</code></td>
                <td class="py-3 px-4">Auto-refresh interval for materialized views</td>
                <td class="py-3 px-4"><code>&#64;&#64;refreshInterval("1h")</code></td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>

      <!-- Best Practices -->
      <section>
        <h2 class="text-2xl font-semibold mb-4">Best Practices</h2>
        <CodeBlock code={viewBestPractices} lang="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">Index Base Tables</h4>
            <p class="text-muted text-sm">
              Views inherit the performance characteristics of their underlying queries.
              Ensure proper indexes exist on columns used in WHERE clauses, JOINs, and GROUP BY.
            </p>
          </div>
          <div class="p-4 rounded-xl bg-surface border border-border">
            <h4 class="font-semibold mb-2 text-primary-400">Use Materialized Views for Heavy Aggregations</h4>
            <p class="text-muted text-sm">
              If a view performs expensive aggregations over large datasets, consider making it
              materialized and refreshing on a schedule rather than computing on every query.
            </p>
          </div>
          <div class="p-4 rounded-xl bg-surface border border-border">
            <h4 class="font-semibold mb-2 text-primary-400">Keep Views Focused</h4>
            <p class="text-muted text-sm">
              Create multiple focused views rather than one complex view. This improves
              maintainability, performance, and makes the API clearer.
            </p>
          </div>
        </div>
      </section>
    </div>
  </article>
</DocsLayout>