Skip to main content

max / makenotwork

mt-db: compile-time checked SQL via sqlx macros (ultra-fuzz M3, A+) Convert all 116 static queries in queries.rs and mutations.rs from runtime sqlx::query*::<_,T>("...") to the query!/query_as!/query_scalar! macros, which verify SQL against the schema at build time. A migration that drops or renames a column now fails cargo check instead of erroring at runtime — closing the M3 type-safety gap (Storage axis). Five queries stay runtime-checked, each with a comment: two dynamic-ORDER-BY list queries and two format!-built multi-row INSERTs (macros require a literal), and create_community_ban (binds a chrono timestamp; with the session store forcing sqlx's `time` feature into the unified build, a bind parameter's type cannot be overridden the way an output column's can). TIMESTAMPTZ output columns carry explicit `chrono::DateTime<chrono::Utc>` overrides because tower-sessions-sqlx-store pulls in sqlx's `time` feature, so the full binary build has both `time` and `chrono` on and the macro otherwise infers OffsetDateTime. Commit the .sqlx/ offline cache and default SQLX_OFFLINE=true so every build machine checks against the cache, never a live DB. Regenerate with `cargo sqlx prepare --workspace` after changing any macro query. Gate: cargo check (full binary, both features) + offline build + clippy --workspace --all-targets clean; 254 integration + 141 unit tests green.
Co-Authored-By
Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-22 21:06 UTC
Signed with PGP, not checked
Commit: 54589bd0e746529bd8ad77c51c1e208351aa6abf
Parent: 4d7b285
114 files changed, +3497 insertions, -239 deletions
M .gitignore +4
@@ -57,3 +57,7 @@
57 57 sando/daemon/work/
58 58 sando/daemon/releases/
59 59 sando/daemon/cargo-target/
60 +
61 + # mt-db uses sqlx compile-time macros; its offline query cache must be committed
62 + # so build hosts (and the Sando gate) compile without a live DB.
63 + !multithreaded/.sqlx/
@@ -13,17 +13,16 @@
13 13 slug: &str,
14 14 description: Option<&str>,
15 15 ) -> Result<Uuid, sqlx::Error> {
16 - let row: (Uuid,) = sqlx::query_as(
16 + sqlx::query_scalar!(
17 17 "INSERT INTO communities (name, slug, description)
18 18 VALUES ($1, $2, $3)
19 19 RETURNING id",
20 + name,
21 + slug,
22 + description,
20 23 )
21 - .bind(name)
22 - .bind(slug)
23 - .bind(description)
24 24 .fetch_one(pool)
25 - .await?;
26 - Ok(row.0)
25 + .await
27 26 }
28 27
29 28 /// Upsert a user from MNW account data. Creates the user if they don't exist,
@@ -35,15 +34,15 @@
35 34 username: &str,
36 35 display_name: Option<&str>,
37 36 ) -> Result<(), sqlx::Error> {
38 - sqlx::query(
37 + sqlx::query!(
39 38 "INSERT INTO users (mnw_account_id, username, display_name)
40 39 VALUES ($1, $2, $3)
41 40 ON CONFLICT (mnw_account_id) DO UPDATE
42 41 SET username = $2, display_name = $3, updated_at = now()",
42 + mnw_account_id,
43 + username,
44 + display_name,
43 45 )
44 - .bind(mnw_account_id)
45 - .bind(username)
46 - .bind(display_name)
47 46 .execute(pool)
48 47 .await?;
49 48 Ok(())
@@ -58,14 +57,14 @@
58 57 community_id: Uuid,
59 58 role: &str,
60 59 ) -> Result<(), sqlx::Error> {
61 - sqlx::query(
60 + sqlx::query!(
62 61 "INSERT INTO memberships (user_id, community_id, role)
63 62 VALUES ($1, $2, $3)
64 63 ON CONFLICT (user_id, community_id) DO NOTHING",
64 + user_id,
65 + community_id,
66 + role,
65 67 )
66 - .bind(user_id)
67 - .bind(community_id)
68 - .bind(role)
69 68 .execute(pool)
70 69 .await?;
71 70 Ok(())
@@ -89,32 +88,32 @@
89 88 ) -> Result<(Uuid, Uuid), sqlx::Error> {
90 89 let mut tx = pool.begin().await?;
91 90
92 - let thread: (Uuid,) = sqlx::query_as(
91 + let thread_id = sqlx::query_scalar!(
93 92 "INSERT INTO threads (category_id, author_id, title, external_ref)
94 93 VALUES ($1, $2, $3, $4)
95 94 RETURNING id",
95 + category_id,
96 + author_id,
97 + title,
98 + external_ref,
96 99 )
97 - .bind(category_id)
98 - .bind(author_id)
99 - .bind(title)
100 - .bind(external_ref)
101 100 .fetch_one(&mut *tx)
102 101 .await?;
103 102
104 - let post: (Uuid,) = sqlx::query_as(
103 + let post_id = sqlx::query_scalar!(
105 104 "INSERT INTO posts (thread_id, author_id, body_markdown, body_html)
106 105 VALUES ($1, $2, $3, $4)
107 106 RETURNING id",
107 + thread_id,
108 + author_id,
109 + body_markdown,
110 + body_html,
108 111 )
109 - .bind(thread.0)
110 - .bind(author_id)
111 - .bind(body_markdown)
112 - .bind(body_html)
113 112 .fetch_one(&mut *tx)
114 113 .await?;
115 114
116 115 tx.commit().await?;
117 - Ok((thread.0, post.0))
116 + Ok((thread_id, post_id))
118 117 }
119 118
120 119 /// Ensure a user has a membership in a community. Creates a 'member' role if none exists.
@@ -124,13 +123,13 @@
124 123 user_id: Uuid,
125 124 community_id: Uuid,
126 125 ) -> Result<(), sqlx::Error> {
127 - sqlx::query(
126 + sqlx::query!(
128 127 "INSERT INTO memberships (user_id, community_id, role)
129 128 VALUES ($1, $2, 'member')
130 129 ON CONFLICT (user_id, community_id) DO NOTHING",
130 + user_id,
131 + community_id,
131 132 )
132 - .bind(user_id)
133 - .bind(community_id)
134 133 .execute(pool)
135 134 .await?;
136 135 Ok(())
@@ -149,17 +148,16 @@
149 148 author_id: Uuid,
150 149 title: &str,
151 150 ) -> Result<Uuid, sqlx::Error> {
152 - let row: (Uuid,) = sqlx::query_as(
151 + sqlx::query_scalar!(
153 152 "INSERT INTO threads (category_id, author_id, title)
154 153 VALUES ($1, $2, $3)
155 154 RETURNING id",
155 + category_id,
156 + author_id,
157 + title,
156 158 )
157 - .bind(category_id)
158 - .bind(author_id)
159 - .bind(title)
160 159 .fetch_one(pool)
161 - .await?;
162 - Ok(row.0)
160 + .await
163 161 }
164 162
165 163 /// Atomically create a thread and its opening post in one transaction.
@@ -178,31 +176,31 @@
178 176 ) -> Result<(Uuid, Uuid), sqlx::Error> {
179 177 let mut tx = pool.begin().await?;
180 178
181 - let thread: (Uuid,) = sqlx::query_as(
179 + let thread_id = sqlx::query_scalar!(
182 180 "INSERT INTO threads (category_id, author_id, title)
183 181 VALUES ($1, $2, $3)
184 182 RETURNING id",
183 + category_id,
184 + author_id,
185 + title,
185 186 )
186 - .bind(category_id)
187 - .bind(author_id)
188 - .bind(title)
189 187 .fetch_one(&mut *tx)
190 188 .await?;
191 189
192 - let post: (Uuid,) = sqlx::query_as(
190 + let post_id = sqlx::query_scalar!(
193 191 "INSERT INTO posts (thread_id, author_id, body_markdown, body_html)
194 192 VALUES ($1, $2, $3, $4)
195 193 RETURNING id",
194 + thread_id,
195 + author_id,
196 + body_markdown,
197 + body_html,
196 198 )
197 - .bind(thread.0)
198 - .bind(author_id)
199 - .bind(body_markdown)
200 - .bind(body_html)
201 199 .fetch_one(&mut *tx)
202 200 .await?;
203 201
204 202 tx.commit().await?;
205 - Ok((thread.0, post.0))
203 + Ok((thread_id, post_id))
206 204 }
207 205
208 206 /// Insert a reply and bump the thread's last_activity_at atomically.
@@ -221,25 +219,24 @@
221 219 ) -> Result<Uuid, sqlx::Error> {
222 220 let mut tx = pool.begin().await?;
223 221
224 - let row: (Uuid,) = sqlx::query_as(
222 + let post_id = sqlx::query_scalar!(
225 223 "INSERT INTO posts (thread_id, author_id, body_markdown, body_html)
226 224 VALUES ($1, $2, $3, $4)
227 225 RETURNING id",
226 + thread_id,
227 + author_id,
228 + body_markdown,
229 + body_html,
228 230 )
229 - .bind(thread_id)
230 - .bind(author_id)
231 - .bind(body_markdown)
232 - .bind(body_html)
233 231 .fetch_one(&mut *tx)
234 232 .await?;
235 233
236 - sqlx::query("UPDATE threads SET last_activity_at = now() WHERE id = $1")
237 - .bind(thread_id)
234 + sqlx::query!("UPDATE threads SET last_activity_at = now() WHERE id = $1", thread_id)
238 235 .execute(&mut *tx)
239 236 .await?;
240 237
241 238 tx.commit().await?;
242 - Ok(row.0)
239 + Ok(post_id)
243 240 }
244 241
245 242 /// Idempotent reply insert keyed on `external_ref`, for the internal API.
@@ -262,37 +259,37 @@
262 259 ) -> Result<(Uuid, bool), sqlx::Error> {
263 260 let mut tx = pool.begin().await?;
264 261
265 - let inserted: Option<(Uuid,)> = sqlx::query_as(
262 + let inserted = sqlx::query_scalar!(
266 263 "INSERT INTO posts (thread_id, author_id, body_markdown, body_html, external_ref)
267 264 VALUES ($1, $2, $3, $4, $5)
268 265 ON CONFLICT (external_ref) WHERE external_ref IS NOT NULL DO NOTHING
269 266 RETURNING id",
267 + thread_id,
268 + author_id,
269 + body_markdown,
270 + body_html,
271 + external_ref,
270 272 )
271 - .bind(thread_id)
272 - .bind(author_id)
273 - .bind(body_markdown)
274 - .bind(body_html)
275 - .bind(external_ref)
276 273 .fetch_optional(&mut *tx)
277 274 .await?;
278 275
279 276 let result = match inserted {
280 - Some((id,)) => {
277 + Some(id) => {
281 278 // Fresh reply: bump thread activity (the trigger already counted it).
282 - sqlx::query("UPDATE threads SET last_activity_at = now() WHERE id = $1")
283 - .bind(thread_id)
279 + sqlx::query!("UPDATE threads SET last_activity_at = now() WHERE id = $1", thread_id)
284 280 .execute(&mut *tx)
285 281 .await?;
286 282 (id, true)
287 283 }
288 284 None => {
289 285 // Replay: the reply already exists. Return its id, don't re-bump.
290 - let existing: (Uuid,) =
291 - sqlx::query_as("SELECT id FROM posts WHERE external_ref = $1")
292 - .bind(external_ref)
293 - .fetch_one(&mut *tx)
294 - .await?;
295 - (existing.0, false)
286 + let existing = sqlx::query_scalar!(
287 + "SELECT id FROM posts WHERE external_ref = $1",
288 + external_ref,
289 + )
290 + .fetch_one(&mut *tx)
291 + .await?;
292 + (existing, false)
296 293 }
297 294 };
298 295
@@ -309,18 +306,17 @@
309 306 body_markdown: &str,
310 307 body_html: &str,
311 308 ) -> Result<Uuid, sqlx::Error> {
312 - let row: (Uuid,) = sqlx::query_as(
309 + sqlx::query_scalar!(
313 310 "INSERT INTO post_footnotes (post_id, author_id, body_markdown, body_html)
314 311 VALUES ($1, $2, $3, $4)
315 312 RETURNING id",
313 + post_id,
314 + author_id,
315 + body_markdown,
316 + body_html,
316 317 )
317 - .bind(post_id)
318 - .bind(author_id)
319 - .bind(body_markdown)
320 - .bind(body_html)
321 318 .fetch_one(pool)
322 - .await?;
323 - Ok(row.0)
319 + .await
324 320 }
325 321
326 322 /// Mod-remove a post: set removed_by/removed_at. Content stays intact for audit.
@@ -331,12 +327,12 @@
331 327 post_id: Uuid,
332 328 removed_by_id: Uuid,
333 329 ) -> Result<bool, sqlx::Error> {
334 - let result = sqlx::query(
330 + let result = sqlx::query!(
335 331 "UPDATE posts SET removed_by = $2, removed_at = now()
336 332 WHERE id = $1 AND removed_at IS NULL",
333 + post_id,
334 + removed_by_id,
337 335 )
338 - .bind(post_id)
339 - .bind(removed_by_id)
340 336 .execute(pool)
341 337 .await?;
342 338 Ok(result.rows_affected() > 0)
@@ -369,12 +365,12 @@
369 365 ) -> Result<PostRemoval, sqlx::Error> {
370 366 let mut tx = pool.begin().await?;
371 367
372 - let post_removed = sqlx::query(
368 + let post_removed = sqlx::query!(
373 369 "UPDATE posts SET removed_by = $2, removed_at = now()
374 370 WHERE id = $1 AND removed_at IS NULL",
371 + post_id,
372 + removed_by_id,
375 373 )
376 - .bind(post_id)
377 - .bind(removed_by_id)
378 374 .execute(&mut *tx)
379 375 .await?
380 376 .rows_affected()
@@ -383,7 +379,7 @@
383 379 let mut thread_removed = false;
384 380 if post_removed {
385 381 // Soft-delete the thread only when this post is its opening post.
386 - thread_removed = sqlx::query(
382 + thread_removed = sqlx::query!(
387 383 "UPDATE threads SET deleted_at = now()
388 384 WHERE deleted_at IS NULL
389 385 AND id = (SELECT thread_id FROM posts WHERE id = $1)
@@ -393,8 +389,8 @@
393 389 ORDER BY created_at ASC, id ASC
394 390 LIMIT 1
395 391 )",
392 + post_id,
396 393 )
397 - .bind(post_id)
398 394 .execute(&mut *tx)
399 395 .await?
400 396 .rows_affected()
@@ -415,13 +411,13 @@
415 411 post_id: Uuid,
416 412 threshold: i32,
417 413 ) -> Result<bool, sqlx::Error> {
418 - let result = sqlx::query(
414 + let result = sqlx::query!(
419 415 "UPDATE posts SET removed_by = NULL, removed_at = now()
420 416 WHERE id = $1 AND removed_at IS NULL
421 417 AND (SELECT COUNT(*) FROM post_flags WHERE post_id = $1 AND resolved_at IS NULL) >= $2",
418 + post_id,
419 + threshold as i64,
422 420 )
423 - .bind(post_id)
424 - .bind(threshold as i64)
425 421 .execute(pool)
426 422 .await?;
427 423 Ok(result.rows_affected() > 0)
@@ -434,9 +430,7 @@
434 430 thread_id: Uuid,
435 431 title: &str,
436 432 ) -> Result<(), sqlx::Error> {
437 - sqlx::query("UPDATE threads SET title = $2 WHERE id = $1")
438 - .bind(thread_id)
439 - .bind(title)
433 + sqlx::query!("UPDATE threads SET title = $2 WHERE id = $1", thread_id, title)
440 434 .execute(pool)
441 435 .await?;
442 436 Ok(())
@@ -445,8 +439,7 @@
445 439 /// Soft-delete a thread: set deleted_at (hides from listings).
446 440 #[tracing::instrument(skip_all)]
447 441 pub async fn soft_delete_thread(pool: &PgPool, thread_id: Uuid) -> Result<(), sqlx::Error> {
448 - sqlx::query("UPDATE threads SET deleted_at = now() WHERE id = $1")
449 - .bind(thread_id)
442 + sqlx::query!("UPDATE threads SET deleted_at = now() WHERE id = $1", thread_id)
450 443 .execute(pool)
451 444 .await?;
452 445 Ok(())
@@ -459,9 +452,7 @@
459 452 thread_id: Uuid,
460 453 pinned: bool,
461 454 ) -> Result<(), sqlx::Error> {
462 - sqlx::query("UPDATE threads SET pinned = $2 WHERE id = $1")
463 - .bind(thread_id)
464 - .bind(pinned)
455 + sqlx::query!("UPDATE threads SET pinned = $2 WHERE id = $1", thread_id, pinned)
465 456 .execute(pool)
466 457 .await?;
467 458 Ok(())
@@ -474,9 +465,7 @@
474 465 thread_id: Uuid,
475 466 locked: bool,
476 467 ) -> Result<(), sqlx::Error> {
477 - sqlx::query("UPDATE threads SET locked = $2 WHERE id = $1")
478 - .bind(thread_id)
479 - .bind(locked)
468 + sqlx::query!("UPDATE threads SET locked = $2 WHERE id = $1", thread_id, locked)
480 469 .execute(pool)
481 470 .await?;
482 471 Ok(())
@@ -491,13 +480,13 @@
491 480 description: Option<&str>,
492 481 auto_hide_threshold: Option<i32>,
493 482 ) -> Result<(), sqlx::Error> {
494 - sqlx::query(
483 + sqlx::query!(
495 484 "UPDATE communities SET name = $2, description = $3, auto_hide_threshold = $4 WHERE id = $1",
485 + community_id,
486 + name,
487 + description,
488 + auto_hide_threshold,
496 489 )
497 - .bind(community_id)
498 - .bind(name)
499 - .bind(description)
500 - .bind(auto_hide_threshold)
501 490 .execute(pool)
502 491 .await?;
503 492 Ok(())
@@ -513,19 +502,18 @@
513 502 description: Option<&str>,
514 503 sort_order: i32,
515 504 ) -> Result<Uuid, sqlx::Error> {
516 - let row: (Uuid,) = sqlx::query_as(
505 + sqlx::query_scalar!(
517 506 "INSERT INTO categories (community_id, name, slug, description, sort_order)
518 507 VALUES ($1, $2, $3, $4, $5)
519 508 RETURNING id",
509 + community_id,
510 + name,
511 + slug,
512 + description,
513 + sort_order,
520 514 )
521 - .bind(community_id)
522 - .bind(name)
523 - .bind(slug)
524 - .bind(description)
525 - .bind(sort_order)
526 515 .fetch_one(pool)
527 - .await?;
528 - Ok(row.0)
516 + .await
529 517 }
530 518
531 519 /// Update a category's name and description (scoped to community).
@@ -537,13 +525,13 @@
537 525 name: &str,
538 526 description: Option<&str>,
539 527 ) -> Result<bool, sqlx::Error> {
540 - let result = sqlx::query(
528 + let result = sqlx::query!(
541 529 "UPDATE categories SET name = $2, description = $3 WHERE id = $1 AND community_id = $4",
530 + category_id,
531 + name,
532 + description,
533 + community_id,
542 534 )
543 - .bind(category_id)
544 - .bind(name)
545 - .bind(description)
546 - .bind(community_id)
547 535 .execute(pool)
548 536 .await?;
549 537 Ok(result.rows_affected() > 0)
@@ -559,14 +547,10 @@
559 547 order_b: i32,
560 548 ) -> Result<(), sqlx::Error> {
561 549 let mut tx = pool.begin().await?;
562 - sqlx::query("UPDATE categories SET sort_order = $2 WHERE id = $1")
563 - .bind(id_a)
564 - .bind(order_b)
550 + sqlx::query!("UPDATE categories SET sort_order = $2 WHERE id = $1", id_a, order_b)
565 551 .execute(&mut *tx)
566 552 .await?;
567 - sqlx::query("UPDATE categories SET sort_order = $2 WHERE id = $1")
568 - .bind(id_b)
569 - .bind(order_a)
553 + sqlx::query!("UPDATE categories SET sort_order = $2 WHERE id = $1", id_b, order_a)
570 554 .execute(&mut *tx)
571 555 .await?;
572 556 tx.commit().await?;
@@ -580,17 +564,16 @@
580 564 community_slug: &str,
581 565 category_slug: &str,
582 566 ) -> Result<Option<Uuid>, sqlx::Error> {
583 - let row: Option<(Uuid,)> = sqlx::query_as(
567 + sqlx::query_scalar!(
584 568 "SELECT c.id
585 569 FROM categories c
586 570 JOIN communities co ON co.id = c.community_id
587 571 WHERE co.slug = $1 AND c.slug = $2",
572 + community_slug,
573 + category_slug,
588 574 )
589 - .bind(community_slug)
590 - .bind(category_slug)
591 575 .fetch_optional(pool)
592 - .await?;
593 - Ok(row.map(|r| r.0))
576 + .await
594 577 }
595 578
596 579 // ============================================================================
@@ -608,7 +591,14 @@
608 591 reason: Option<&str>,
609 592 expires_at: Option<DateTime<Utc>>,
610 593 ) -> Result<Uuid, sqlx::Error> {
611 - let row: (Uuid,) = sqlx::query_as(
594 + // runtime-checked: the full server build unifies sqlx's `time` and `chrono`
595 + // features (the session store pulls in `time`), and with both on the
596 + // compile-time macro infers the TIMESTAMPTZ bind parameter as
597 + // `time::OffsetDateTime`, which a `chrono::DateTime<Utc>` won't satisfy.
598 + // Output-column type can be overridden in the macro; a bind parameter's
599 + // cannot. This is the one write path that binds a chrono timestamp, so it
Lines truncated
@@ -123,11 +123,12 @@
123 123 community_id: Uuid,
124 124 category_slug: &str,
125 125 ) -> Result<Option<CategoryIdRow>, sqlx::Error> {
126 - sqlx::query_as::<_, CategoryIdRow>(
126 + sqlx::query_as!(
127 + CategoryIdRow,
127 128 "SELECT id, name, slug FROM categories WHERE community_id = $1 AND slug = $2",
129 + community_id,
130 + category_slug,
128 131 )
129 - .bind(community_id)
130 - .bind(category_slug)
131 132 .fetch_optional(pool)
132 133 .await
133 134 }
@@ -138,12 +139,13 @@
138 139 pool: &PgPool,
139 140 external_ref: &str,
140 141 ) -> Result<Option<(Uuid,)>, sqlx::Error> {
141 - sqlx::query_as::<_, (Uuid,)>(
142 + sqlx::query!(
142 143 "SELECT id FROM threads WHERE external_ref = $1",
144 + external_ref,
143 145 )
144 - .bind(external_ref)
145 146 .fetch_optional(pool)
146 147 .await
148 + .map(|opt| opt.map(|r| (r.id,)))
147 149 }
148 150
149 151 /// Get thread stats: post count and last activity timestamp.
@@ -158,16 +160,17 @@
158 160 // excluding mod-removed and soft-deleted posts (matching the m029 trigger and
159 161 // the profile tallies). Contrast `count_posts_in_thread`, which deliberately
160 162 // counts tombstones because the in-app thread list renders them.
161 - sqlx::query_as::<_, (i64, Option<DateTime<Utc>>)>(
162 - "SELECT COUNT(p.id), MAX(p.created_at)
163 - FROM posts p
164 - WHERE p.thread_id = $1
165 - AND p.removed_at IS NULL
166 - AND p.deleted_at IS NULL",
163 + sqlx::query!(
164 + r#"SELECT COUNT(p.id) AS "count!", MAX(p.created_at) AS "max_created: chrono::DateTime<chrono::Utc>"
165 + FROM posts p
166 + WHERE p.thread_id = $1
167 + AND p.removed_at IS NULL
168 + AND p.deleted_at IS NULL"#,
169 + thread_id,
167 170 )
168 - .bind(thread_id)
169 171 .fetch_optional(pool)
170 172 .await
173 + .map(|opt| opt.map(|r| (r.count, r.max_created)))
171 174 }
172 175
173 176 // ============================================================================
@@ -189,26 +192,27 @@
189 192 /// [`list_archived_communities`] for the explicit archived view.
190 193 #[tracing::instrument(skip_all)]
191 194 pub async fn list_communities(pool: &PgPool, limit: i64, offset: i64) -> Result<Vec<CommunityListRow>, sqlx::Error> {
192 - sqlx::query_as::<_, CommunityListRow>(
195 + sqlx::query_as!(
196 + CommunityListRow,
193 197 // Per-community counts as scalar subqueries rather than a
194 198 // categories⋈threads join + GROUP BY COUNT(DISTINCT): the join
195 199 // multiplied rows (community × categories × threads) and forced
196 200 // aggregating *every* community's threads before LIMIT. As scalar
197 201 // subqueries in the target list they're evaluated only for the page's
198 202 // output rows (after ORDER BY + LIMIT), bounding the work to one page.
199 - "SELECT co.name, co.slug, co.description,
200 - (SELECT COUNT(*) FROM categories c WHERE c.community_id = co.id) AS category_count,
203 + r#"SELECT co.name, co.slug, co.description,
204 + (SELECT COUNT(*) FROM categories c WHERE c.community_id = co.id) AS "category_count!",
201 205 (SELECT COUNT(*) FROM threads t
202 206 JOIN categories c2 ON c2.id = t.category_id
203 - WHERE c2.community_id = co.id) AS thread_count
207 + WHERE c2.community_id = co.id) AS "thread_count!"
204 208 FROM communities co
205 209 WHERE co.suspended_at IS NULL
206 210 AND co.state <> 'archived'
207 211 ORDER BY co.name
208 - LIMIT $1 OFFSET $2",
212 + LIMIT $1 OFFSET $2"#,
213 + limit,
214 + offset,
209 215 )
210 - .bind(limit)
211 - .bind(offset)
212 216 .fetch_all(pool)
213 217 .await
214 218 }
@@ -216,8 +220,8 @@
216 220 /// Count non-suspended, non-archived communities.
217 221 #[tracing::instrument(skip_all)]
218 222 pub async fn count_communities(pool: &PgPool) -> Result<i64, sqlx::Error> {
219 - sqlx::query_scalar(
220 - "SELECT COUNT(*) FROM communities WHERE suspended_at IS NULL AND state <> 'archived'",
223 + sqlx::query_scalar!(
224 + r#"SELECT COUNT(*) AS "count!" FROM communities WHERE suspended_at IS NULL AND state <> 'archived'"#,
221 225 )
222 226 .fetch_one(pool)
223 227 .await
@@ -231,29 +235,30 @@
231 235 limit: i64,
232 236 offset: i64,
233 237 ) -> Result<Vec<CommunityListRow>, sqlx::Error> {
234 - sqlx::query_as::<_, CommunityListRow>(
238 + sqlx::query_as!(
239 + CommunityListRow,
235 240 // Scalar-subquery counts (see `list_communities`) — bounded to the page.
236 - "SELECT co.name, co.slug, co.description,
237 - (SELECT COUNT(*) FROM categories c WHERE c.community_id = co.id) AS category_count,
241 + r#"SELECT co.name, co.slug, co.description,
242 + (SELECT COUNT(*) FROM categories c WHERE c.community_id = co.id) AS "category_count!",
238 243 (SELECT COUNT(*) FROM threads t
239 244 JOIN categories c2 ON c2.id = t.category_id
240 - WHERE c2.community_id = co.id) AS thread_count
245 + WHERE c2.community_id = co.id) AS "thread_count!"
241 246 FROM communities co
242 247 WHERE co.suspended_at IS NULL
243 248 AND co.state = 'archived'
244 249 ORDER BY co.name
245 - LIMIT $1 OFFSET $2",
250 + LIMIT $1 OFFSET $2"#,
251 + limit,
252 + offset,
246 253 )
247 - .bind(limit)
248 - .bind(offset)
249 254 .fetch_all(pool)
250 255 .await
251 256 }
252 257
253 258 #[tracing::instrument(skip_all)]
254 259 pub async fn count_archived_communities(pool: &PgPool) -> Result<i64, sqlx::Error> {
255 - sqlx::query_scalar(
256 - "SELECT COUNT(*) FROM communities WHERE suspended_at IS NULL AND state = 'archived'",
260 + sqlx::query_scalar!(
261 + r#"SELECT COUNT(*) AS "count!" FROM communities WHERE suspended_at IS NULL AND state = 'archived'"#,
257 262 )
258 263 .fetch_one(pool)
259 264 .await
@@ -264,11 +269,15 @@
264 269 pool: &PgPool,
265 270 slug: &str,
266 271 ) -> Result<Option<CommunityRow>, sqlx::Error> {
267 - sqlx::query_as::<_, CommunityRow>(
268 - "SELECT id, name, slug, description, suspended_at, auto_hide_threshold, state
269 - FROM communities WHERE slug = $1",
272 + sqlx::query_as!(
273 + CommunityRow,
274 + r#"SELECT id, name, slug, description,
275 + suspended_at AS "suspended_at: chrono::DateTime<chrono::Utc>",
276 + auto_hide_threshold,
277 + state AS "state: CommunityState"
278 + FROM communities WHERE slug = $1"#,
279 + slug,
270 280 )
271 - .bind(slug)
272 281 .fetch_optional(pool)
273 282 .await
274 283 }
@@ -278,11 +287,15 @@
278 287 pool: &PgPool,
279 288 id: Uuid,
280 289 ) -> Result<Option<CommunityRow>, sqlx::Error> {
281 - sqlx::query_as::<_, CommunityRow>(
282 - "SELECT id, name, slug, description, suspended_at, auto_hide_threshold, state
283 - FROM communities WHERE id = $1",
290 + sqlx::query_as!(
291 + CommunityRow,
292 + r#"SELECT id, name, slug, description,
293 + suspended_at AS "suspended_at: chrono::DateTime<chrono::Utc>",
294 + auto_hide_threshold,
295 + state AS "state: CommunityState"
296 + FROM communities WHERE id = $1"#,
297 + id,
284 298 )
285 - .bind(id)
286 299 .fetch_optional(pool)
287 300 .await
288 301 }
@@ -292,17 +305,18 @@
292 305 pool: &PgPool,
293 306 community_slug: &str,
294 307 ) -> Result<Vec<CategoryWithCount>, sqlx::Error> {
295 - sqlx::query_as::<_, CategoryWithCount>(
296 - "SELECT c.name, c.slug, c.description,
297 - COUNT(t.id) AS thread_count
308 + sqlx::query_as!(
309 + CategoryWithCount,
310 + r#"SELECT c.name, c.slug, c.description,
311 + COUNT(t.id) AS "thread_count!"
298 312 FROM categories c
299 313 JOIN communities co ON co.id = c.community_id
300 314 LEFT JOIN threads t ON t.category_id = c.id AND t.deleted_at IS NULL
301 315 WHERE co.slug = $1
302 316 GROUP BY c.id, c.name, c.slug, c.description, c.sort_order
303 - ORDER BY c.sort_order",
317 + ORDER BY c.sort_order"#,
318 + community_slug,
304 319 )
305 - .bind(community_slug)
306 320 .fetch_all(pool)
307 321 .await
308 322 }
@@ -313,14 +327,15 @@
313 327 community_slug: &str,
314 328 category_slug: &str,
315 329 ) -> Result<Option<CategoryRow>, sqlx::Error> {
316 - sqlx::query_as::<_, CategoryRow>(
330 + sqlx::query_as!(
331 + CategoryRow,
317 332 "SELECT c.name, c.slug
318 333 FROM categories c
319 334 JOIN communities co ON co.id = c.community_id
320 335 WHERE co.slug = $1 AND c.slug = $2",
336 + community_slug,
337 + category_slug,
321 338 )
322 - .bind(community_slug)
323 - .bind(category_slug)
324 339 .fetch_optional(pool)
325 340 .await
326 341 }
@@ -333,12 +348,13 @@
333 348 limit: i64,
334 349 offset: i64,
335 350 ) -> Result<Vec<ThreadWithMeta>, sqlx::Error> {
336 - sqlx::query_as::<_, ThreadWithMeta>(
337 - "SELECT t.id, t.title,
338 - COALESCE(u.display_name, u.username) AS author_name,
351 + sqlx::query_as!(
352 + ThreadWithMeta,
353 + r#"SELECT t.id, t.title,
354 + COALESCE(u.display_name, u.username) AS "author_name!",
339 355 u.username AS author_username,
340 - GREATEST(t.post_count - 1, 0)::BIGINT AS reply_count,
341 - t.last_activity_at,
356 + GREATEST(t.post_count - 1, 0)::BIGINT AS "reply_count!",
357 + t.last_activity_at AS "last_activity_at: chrono::DateTime<chrono::Utc>",
342 358 t.pinned, t.locked
343 359 FROM threads t
344 360 JOIN categories c ON c.id = t.category_id
@@ -346,12 +362,12 @@
346 362 JOIN users u ON u.mnw_account_id = t.author_id
347 363 WHERE co.slug = $1 AND c.slug = $2 AND t.deleted_at IS NULL
348 364 ORDER BY t.pinned DESC, t.last_activity_at DESC
349 - LIMIT $3 OFFSET $4",
365 + LIMIT $3 OFFSET $4"#,
366 + community_slug,
367 + category_slug,
368 + limit,
369 + offset,
350 370 )
351 - .bind(community_slug)
352 - .bind(category_slug)
353 - .bind(limit)
354 - .bind(offset)
355 371 .fetch_all(pool)
356 372 .await
357 373 }
@@ -391,6 +407,7 @@
391 407 LIMIT $3 OFFSET $4"
392 408 );
393 409
410 + // runtime-checked: dynamic SQL (cannot use compile-time macro)
394 411 sqlx::query_as::<_, ThreadWithMeta>(&query)
395 412 .bind(community_slug)
396 413 .bind(category_slug)
@@ -406,15 +423,15 @@
406 423 community_slug: &str,
407 424 category_slug: &str,
408 425 ) -> Result<i64, sqlx::Error> {
409 - sqlx::query_scalar(
410 - "SELECT COUNT(*)
426 + sqlx::query_scalar!(
427 + r#"SELECT COUNT(*) AS "count!"
411 428 FROM threads t
412 429 JOIN categories c ON c.id = t.category_id
413 430 JOIN communities co ON co.id = c.community_id
414 - WHERE co.slug = $1 AND c.slug = $2 AND t.deleted_at IS NULL",
431 + WHERE co.slug = $1 AND c.slug = $2 AND t.deleted_at IS NULL"#,
432 + community_slug,
433 + category_slug,
415 434 )
416 - .bind(community_slug)
417 - .bind(category_slug)
418 435 .fetch_one(pool)
419 436 .await
420 437 }
@@ -424,17 +441,18 @@
424 441 pool: &PgPool,
425 442 thread_id: Uuid,
426 443 ) -> Result<Option<ThreadWithBreadcrumb>, sqlx::Error> {
427 - sqlx::query_as::<_, ThreadWithBreadcrumb>(
428 - "SELECT t.id, t.title, t.locked, t.pinned, t.author_id,
444 + sqlx::query_as!(
445 + ThreadWithBreadcrumb,
446 + r#"SELECT t.id, t.title, t.locked, t.pinned, t.author_id,
429 447 co.id AS community_id,
430 448 co.name AS community_name, co.slug AS community_slug,
431 449 c.name AS category_name, c.slug AS category_slug
432 450 FROM threads t
433 451 JOIN categories c ON c.id = t.category_id
434 452 JOIN communities co ON co.id = c.community_id
435 - WHERE t.id = $1 AND t.deleted_at IS NULL",
453 + WHERE t.id = $1 AND t.deleted_at IS NULL"#,
454 + thread_id,
436 455 )
437 - .bind(thread_id)
438 456 .fetch_optional(pool)
439 457 .await
440 458 }
@@ -444,20 +462,24 @@
444 462 pool: &PgPool,
445 463 thread_id: Uuid,
446 464 ) -> Result<Vec<PostWithAuthor>, sqlx::Error> {
447 - sqlx::query_as::<_, PostWithAuthor>(
448 - "SELECT p.id, p.author_id,
449 - COALESCE(u.display_name, u.username) AS author_name,
465 + sqlx::query_as!(
466 + PostWithAuthor,
467 + r#"SELECT p.id, p.author_id,
468 + COALESCE(u.display_name, u.username) AS "author_name!",
450 469 u.username AS author_username,
451 - p.body_html, p.created_at, p.edited_at, p.deleted_at,
452 - p.removed_at,
470 + p.body_html,
471 + p.created_at AS "created_at: chrono::DateTime<chrono::Utc>",
472 + p.edited_at AS "edited_at: chrono::DateTime<chrono::Utc>",
473 + p.deleted_at AS "deleted_at: chrono::DateTime<chrono::Utc>",
474 + p.removed_at AS "removed_at: chrono::DateTime<chrono::Utc>",
453 475 u.is_fan_plus AS author_is_fan_plus,
454 476 u.signature_html AS author_signature_html
455 477 FROM posts p
456 478 JOIN users u ON u.mnw_account_id = p.author_id
457 479 WHERE p.thread_id = $1
458 - ORDER BY p.created_at",
480 + ORDER BY p.created_at"#,
481 + thread_id,
459 482 )
460 - .bind(thread_id)
461 483 .fetch_all(pool)
462 484 .await
463 485 }
@@ -469,23 +491,27 @@
469 491 limit: i64,
470 492 offset: i64,
471 493 ) -> Result<Vec<PostWithAuthor>, sqlx::Error> {
472 - sqlx::query_as::<_, PostWithAuthor>(
473 - "SELECT p.id, p.author_id,
474 - COALESCE(u.display_name, u.username) AS author_name,
494 + sqlx::query_as!(
495 + PostWithAuthor,
496 + r#"SELECT p.id, p.author_id,
497 + COALESCE(u.display_name, u.username) AS "author_name!",
475 498 u.username AS author_username,
476 - p.body_html, p.created_at, p.edited_at, p.deleted_at,
477 - p.removed_at,
499 + p.body_html,
500 + p.created_at AS "created_at: chrono::DateTime<chrono::Utc>",
501 + p.edited_at AS "edited_at: chrono::DateTime<chrono::Utc>",
502 + p.deleted_at AS "deleted_at: chrono::DateTime<chrono::Utc>",
503 + p.removed_at AS "removed_at: chrono::DateTime<chrono::Utc>",
478 504 u.is_fan_plus AS author_is_fan_plus,
479 505 u.signature_html AS author_signature_html
480 506 FROM posts p
481 507 JOIN users u ON u.mnw_account_id = p.author_id
482 508 WHERE p.thread_id = $1
483 509 ORDER BY p.created_at
484 - LIMIT $2 OFFSET $3",
510 + LIMIT $2 OFFSET $3"#,
511 + thread_id,
512 + limit,
513 + offset,
485 514 )
486 - .bind(thread_id)
487 - .bind(limit)
488 - .bind(offset)
489 515 .fetch_all(pool)
490 516 .await
491 517 }
@@ -500,10 +526,10 @@
500 526 // returns those rows so they render as tombstones. The count must match the
501 527 // list it paginates, or the last page of tombstones would be cut off. For an
502 528 // active-only count (e.g. the server-facing stats), use `get_thread_stats`.
503 - sqlx::query_scalar(
504 - "SELECT COUNT(*) FROM posts WHERE thread_id = $1",
529 + sqlx::query_scalar!(
530 + r#"SELECT COUNT(*) AS "count!" FROM posts WHERE thread_id = $1"#,
531 + thread_id,
505 532 )
506 - .bind(thread_id)
507 533 .fetch_one(pool)
508 534 .await
509 535 }
@@ -515,12 +541,12 @@
515 541 user_id: Uuid,
516 542 seconds: i64,
517 543 ) -> Result<i64, sqlx::Error> {
518 - sqlx::query_scalar(
519 - "SELECT (SELECT COUNT(*) FROM posts WHERE author_id = $1 AND created_at > NOW() - make_interval(secs => $2))
520 - + (SELECT COUNT(*) FROM post_footnotes WHERE author_id = $1 AND created_at > NOW() - make_interval(secs => $2))",
544 + sqlx::query_scalar!(
545 + r#"SELECT (SELECT COUNT(*) FROM posts WHERE author_id = $1 AND created_at > NOW() - make_interval(secs => $2))
546 + + (SELECT COUNT(*) FROM post_footnotes WHERE author_id = $1 AND created_at > NOW() - make_interval(secs => $2)) AS "count!""#,
547 + user_id,
548 + seconds as f64,
521 549 )
522 - .bind(user_id)
523 - .bind(seconds as f64)
524 550 .fetch_one(pool)
525 551 .await
526 552 }
@@ -530,8 +556,11 @@
530 556 pool: &PgPool,
531 557 post_id: Uuid,
532 558 ) -> Result<Option<PostForEdit>, sqlx::Error> {
533 - sqlx::query_as::<_, PostForEdit>(
534 - "SELECT p.id, p.author_id, p.body_markdown, p.created_at, p.deleted_at,
559 + sqlx::query_as!(
560 + PostForEdit,
561 + r#"SELECT p.id, p.author_id, p.body_markdown,
562 + p.created_at AS "created_at: chrono::DateTime<chrono::Utc>",
563 + p.deleted_at AS "deleted_at: chrono::DateTime<chrono::Utc>",
535 564 p.thread_id, t.title AS thread_title,
536 565 co.name AS community_name, co.slug AS community_slug,
537 566 co.id AS community_id,
@@ -540,9 +569,9 @@
540 569 JOIN threads t ON t.id = p.thread_id
541 570 JOIN categories c ON c.id = t.category_id
542 571 JOIN communities co ON co.id = c.community_id
543 - WHERE p.id = $1",
572 + WHERE p.id = $1"#,
573 + post_id,
544 574 )
545 - .bind(post_id)
546 575 .fetch_optional(pool)
547 576 .await
548 577 }
@@ -553,14 +582,13 @@
553 582 user_id: Uuid,
554 583 community_id: Uuid,
555 584 ) -> Result<Option<CommunityRole>, sqlx::Error> {
556 - let row: Option<(CommunityRole,)> = sqlx::query_as(
557 - "SELECT role FROM memberships WHERE user_id = $1 AND community_id = $2",
585 + sqlx::query_scalar!(
586 + r#"SELECT role AS "role: CommunityRole" FROM memberships WHERE user_id = $1 AND community_id = $2"#,
587 + user_id,
588 + community_id,
558 589 )
559 - .bind(user_id)
560 - .bind(community_id)
561 590 .fetch_optional(pool)
562 - .await?;
563 - Ok(row.map(|r| r.0))
591 + .await
564 592 }
565 593
566 594 #[derive(sqlx::FromRow)]
@@ -577,13 +605,14 @@
577 605 pool: &PgPool,
578 606 community_id: Uuid,
579 607 ) -> Result<Vec<CategoryForSettings>, sqlx::Error> {
580 - sqlx::query_as::<_, CategoryForSettings>(
608 + sqlx::query_as!(
609 + CategoryForSettings,
581 610 "SELECT id, name, slug, description, sort_order
582 611 FROM categories
583 612 WHERE community_id = $1
584 613 ORDER BY sort_order",
614 + community_id,
585 615 )
586 - .bind(community_id)
587 616 .fetch_all(pool)
588 617 .await
589 618 }
@@ -603,11 +632,12 @@
603 632 limit: i64,
604 633 offset: i64,
605 634 ) -> Result<Vec<MemberRow>, sqlx::Error> {
606 - sqlx::query_as::<_, MemberRow>(
607 - "SELECT u.username,
635 + sqlx::query_as!(
636 + MemberRow,
637 + r#"SELECT u.username,
608 638 u.display_name,
609 - m.role,
610 - m.joined_at
639 + m.role AS "role: CommunityRole",
640 + m.joined_at AS "joined_at: chrono::DateTime<chrono::Utc>"
611 641 FROM memberships m
612 642 JOIN users u ON u.mnw_account_id = m.user_id
613 643 WHERE m.community_id = $1
@@ -619,11 +649,11 @@
619 649 ELSE 3
620 650 END,
621 651 m.joined_at
622 - LIMIT $2 OFFSET $3",
652 + LIMIT $2 OFFSET $3"#,
653 + community_id,
654 + limit,
655 + offset,
623 656 )
624 - .bind(community_id)
625 - .bind(limit)
626 - .bind(offset)
627 657 .fetch_all(pool)
628 658 .await
629 659 }
@@ -634,10 +664,12 @@
634 664 pool: &PgPool,
635 665 community_id: Uuid,
636 666 ) -> Result<i64, sqlx::Error> {
637 - sqlx::query_scalar("SELECT COUNT(*) FROM memberships WHERE community_id = $1")
638 - .bind(community_id)
639 - .fetch_one(pool)
640 - .await
667 + sqlx::query_scalar!(
668 + r#"SELECT COUNT(*) AS "count!" FROM memberships WHERE community_id = $1"#,
669 + community_id,
670 + )
671 + .fetch_one(pool)
672 + .await
Lines truncated
@@ -1,0 +1,7 @@
1 + # sqlx compile-time query checking uses the committed `.sqlx/` cache, not a live
2 + # database. After changing or adding any `query!`/`query_as!`/`query_scalar!` in
3 + # mt-db, regenerate it with `DATABASE_URL=postgres:///multithreaded cargo sqlx prepare --workspace`
4 + # and commit the result. Override locally with `SQLX_OFFLINE=false` to verify
5 + # against a live DB instead.
6 + [env]
7 + SQLX_OFFLINE = "true"
@@ -1,0 +1,15 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "UPDATE users SET suspended_at = now(), suspension_reason = $2 WHERE mnw_account_id = $1",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": [
8 + "Uuid",
9 + "Text"
10 + ]
11 + },
12 + "nullable": []
13 + },
14 + "hash": "03ade415463b367871d02a5b82d14b7f544ebfbeb40080d3194bcb72fa9a47ae"
15 + }
@@ -1,0 +1,16 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "UPDATE post_flags SET resolved_at = now(), resolved_by = $2, resolution = $3\n WHERE id = $1 AND resolved_at IS NULL",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": [
8 + "Uuid",
9 + "Uuid",
10 + "Text"
11 + ]
12 + },
13 + "nullable": []
14 + },
15 + "hash": "0768d2c0c86038fc01f2b662ab65c9fe45979876679ce94ae6431ff1600ed4ba"
16 + }
@@ -1,0 +1,58 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT f.id, f.post_id, f.author_id,\n COALESCE(u.display_name, u.username) AS \"author_name!\",\n u.username AS author_username,\n f.body_html,\n f.created_at AS \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM post_footnotes f\n JOIN users u ON u.mnw_account_id = f.author_id\n WHERE f.post_id = ANY($1)\n ORDER BY f.created_at",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "post_id",
14 + "type_info": "Uuid"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "author_id",
19 + "type_info": "Uuid"
20 + },
21 + {
22 + "ordinal": 3,
23 + "name": "author_name!",
24 + "type_info": "Text"
25 + },
26 + {
27 + "ordinal": 4,
28 + "name": "author_username",
29 + "type_info": "Text"
30 + },
31 + {
32 + "ordinal": 5,
33 + "name": "body_html",
34 + "type_info": "Text"
35 + },
36 + {
37 + "ordinal": 6,
38 + "name": "created_at: chrono::DateTime<chrono::Utc>",
39 + "type_info": "Timestamptz"
40 + }
41 + ],
42 + "parameters": {
43 + "Left": [
44 + "UuidArray"
45 + ]
46 + },
47 + "nullable": [
48 + false,
49 + false,
50 + false,
51 + null,
52 + false,
53 + false,
54 + false
55 + ]
56 + },
57 + "hash": "0830906e69639e409caf641739ac5ccc16662f9800fdccea7c1c0eeef5193f9e"
58 + }
@@ -1,0 +1,29 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT c.name, c.slug\n FROM categories c\n JOIN communities co ON co.id = c.community_id\n WHERE co.slug = $1 AND c.slug = $2",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "name",
9 + "type_info": "Text"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "slug",
14 + "type_info": "Text"
15 + }
16 + ],
17 + "parameters": {
18 + "Left": [
19 + "Text",
20 + "Text"
21 + ]
22 + },
23 + "nullable": [
24 + false,
25 + false
26 + ]
27 + },
28 + "hash": "0a8b8dc6d47eb404cf7fb308e9cfa9083797b7cc54a716b151532b3c6f0034dc"
29 + }
@@ -1,0 +1,71 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT cb.id, cb.user_id,\n u.username, u.display_name,\n cb.ban_type AS \"ban_type: BanType\", cb.reason,\n cb.expires_at AS \"expires_at: chrono::DateTime<chrono::Utc>\",\n cb.created_at AS \"created_at: chrono::DateTime<chrono::Utc>\",\n actor.username AS banned_by_username\n FROM community_bans cb\n JOIN users u ON u.mnw_account_id = cb.user_id\n JOIN users actor ON actor.mnw_account_id = cb.banned_by\n WHERE cb.community_id = $1\n AND (cb.expires_at IS NULL OR cb.expires_at > now())\n ORDER BY cb.created_at DESC\n LIMIT $2",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "user_id",
14 + "type_info": "Uuid"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "username",
19 + "type_info": "Text"
20 + },
21 + {
22 + "ordinal": 3,
23 + "name": "display_name",
24 + "type_info": "Text"
25 + },
26 + {
27 + "ordinal": 4,
28 + "name": "ban_type: BanType",
29 + "type_info": "Text"
30 + },
31 + {
32 + "ordinal": 5,
33 + "name": "reason",
34 + "type_info": "Text"
35 + },
36 + {
37 + "ordinal": 6,
38 + "name": "expires_at: chrono::DateTime<chrono::Utc>",
39 + "type_info": "Timestamptz"
40 + },
41 + {
42 + "ordinal": 7,
43 + "name": "created_at: chrono::DateTime<chrono::Utc>",
44 + "type_info": "Timestamptz"
45 + },
46 + {
47 + "ordinal": 8,
48 + "name": "banned_by_username",
49 + "type_info": "Text"
50 + }
51 + ],
52 + "parameters": {
53 + "Left": [
54 + "Uuid",
55 + "Int8"
56 + ]
57 + },
58 + "nullable": [
59 + false,
60 + false,
61 + false,
62 + true,
63 + false,
64 + true,
65 + true,
66 + false,
67 + false
68 + ]
69 + },
70 + "hash": "0ba3bb2135932053e74fee53d32e33138819e31a184841df4b365f8e4c32b541"
71 + }
@@ -1,0 +1,15 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "UPDATE communities SET state = $2 WHERE id = $1",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": [
8 + "Uuid",
9 + "Text"
10 + ]
11 + },
12 + "nullable": []
13 + },
14 + "hash": "0c8bd7af0ecbc3729357019179bdd62a54de1c15b51ccb7d10febf32886a3a5a"
15 + }
@@ -1,0 +1,22 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT id FROM posts WHERE external_ref = $1",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + }
11 + ],
12 + "parameters": {
13 + "Left": [
14 + "Text"
15 + ]
16 + },
17 + "nullable": [
18 + false
19 + ]
20 + },
21 + "hash": "0d0bd14a16970a0c34e9e719b087616f5730863904aeed9caab237b8efb15e1f"
22 + }
@@ -1,0 +1,34 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT id, name, slug FROM tags WHERE community_id = $1 ORDER BY name",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "name",
14 + "type_info": "Text"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "slug",
19 + "type_info": "Text"
20 + }
21 + ],
22 + "parameters": {
23 + "Left": [
24 + "Uuid"
25 + ]
26 + },
27 + "nullable": [
28 + false,
29 + false,
30 + false
31 + ]
32 + },
33 + "hash": "0feaed5a9d2365a4a0d18d12d3e52fadf77592fd7c31e1959787dffc87b830f4"
34 + }
@@ -1,0 +1,25 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "INSERT INTO post_footnotes (post_id, author_id, body_markdown, body_html)\n VALUES ($1, $2, $3, $4)\n RETURNING id",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + }
11 + ],
12 + "parameters": {
13 + "Left": [
14 + "Uuid",
15 + "Uuid",
16 + "Text",
17 + "Text"
18 + ]
19 + },
20 + "nullable": [
21 + false
22 + ]
23 + },
24 + "hash": "11529b2b1e38fc99f7732f2080c172fd2691ce72ff8f231d462bf9d7a0d085d6"
25 + }
@@ -1,0 +1,54 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT t.id AS thread_id,\n t.title AS thread_title,\n c.name AS category_name,\n c.slug AS category_slug,\n p.created_at AS \"post_created_at: chrono::DateTime<chrono::Utc>\",\n (t.author_id = $2) AS \"is_thread_author!\"\n FROM posts p\n JOIN threads t ON t.id = p.thread_id\n JOIN categories c ON c.id = t.category_id\n WHERE c.community_id = $1\n AND p.author_id = $2\n AND p.removed_at IS NULL\n AND p.deleted_at IS NULL\n AND t.deleted_at IS NULL\n ORDER BY p.created_at DESC\n LIMIT $3",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "thread_id",
9 + "type_info": "Uuid"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "thread_title",
14 + "type_info": "Text"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "category_name",
19 + "type_info": "Text"
20 + },
21 + {
22 + "ordinal": 3,
23 + "name": "category_slug",
24 + "type_info": "Text"
25 + },
26 + {
27 + "ordinal": 4,
28 + "name": "post_created_at: chrono::DateTime<chrono::Utc>",
29 + "type_info": "Timestamptz"
30 + },
31 + {
32 + "ordinal": 5,
33 + "name": "is_thread_author!",
34 + "type_info": "Bool"
35 + }
36 + ],
37 + "parameters": {
38 + "Left": [
39 + "Uuid",
40 + "Uuid",
41 + "Int8"
42 + ]
43 + },
44 + "nullable": [
45 + false,
46 + false,
47 + false,
48 + false,
49 + false,
50 + null
51 + ]
52 + },
53 + "hash": "150877ff6d3dd6d1c8034f0f5f9af152b5759f9c0b7e0c3549b636be95f65f89"
54 + }