Markdown Files
name: tokio-postgres description: Use ONLY when working with database code (tokio-postgres, rust-postgres, Neon Postgres connections, queries, migrations, connection pools, or DB schema). Covers connection setup, query patterns, type mapping, and Neon-specific TLS config for this project.
tokio-postgres (rust-postgres)
Async PostgreSQL client for Rust. This project uses tokio-postgres with Neon (serverless Postgres).
Dependency
Already in Cargo.toml or should be added:
[dependencies.tokio-postgres]
version = "0.7"
features = ["with-chrono-0_4", "with-uuid-1"]
Neon requires TLS. Use postgres-native-tls (or postgres-openssl):
[dependencies]
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1"] }
postgres-native-tls = "0.5"
native-tls = "0.2"
Neon Connection Pattern
Neon uses a standard Postgres wire protocol over TLS. Connection params come from the .env file using libpq-style env vars (PGHOST, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE), not a DATABASE_URL string.
Current .env values:
PGHOST=ep-holy-mouse-alwcchgn-pooler.c-3.eu-central-1.aws.neon.techPGDATABASE=neondbPGUSER=neondb_ownerPGSSLMODE=require— TLS is mandatory
Building the connection
use postgres_native_tls::MakeTlsConnector;
use native_tls::TlsConnector;
use tokio_postgres::Config;
async fn connect() -> Result<tokio_postgres::Client, Box<dyn std::error::Error>> {
// Build config from libpq env vars (PGHOST, PGUSER, etc.)
let config = tokio_postgres::Config::from_env()?;
let tls = MakeTlsConnector::new(TlsConnector::new()?);
let (client, connection) = config.connect(tls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
Ok(client)
}
Key points:
- Always spawn the connection task — the
Connectionfuture must be polled for queries to execute. - Neon requires TLS —
NoTlswill not work. Always useMakeTlsConnector. - Use
Config::from_env()— readsPGHOST,PGPORT,PGUSER,PGPASSWORD,PGDATABASE,PGSSLMODEfrom.envautomatically. Do not hardcode a connection string. - The
.envhost uses the pooler endpoint (-poolersuffix), which enables Neon's connection pooling.
Query Patterns
Simple queries (no params, no prepared statements)
let rows = client.simple_query("SELECT 1").await?;
Parameterized queries (preferred — prevents SQL injection)
let rows = client
.query("SELECT id, title FROM articles WHERE audience_id = $1", &[&audience_id])
.await?;
for row in &rows {
let id: uuid::Uuid = row.get(0);
let title: &str = row.get(1);
}
Query one row
let row = client
.query_opt("SELECT id, name FROM audiences WHERE slug = $1", &[&slug])
.await?;
if let Some(row) = row {
let id: uuid::Uuid = row.get(0);
let name: &str = row.get(1);
}
Execute (INSERT/UPDATE/DELETE — no rows returned)
client
.execute(
"INSERT INTO articles (id, title, audience_id, content) VALUES ($1, $2, $3, $4)",
&[&id, &title, &audience_id, &content],
)
.await?;
Prepared statements (reuse for repeated queries)
let stmt = client.prepare("SELECT id, title FROM articles WHERE id = $1").await?;
let rows = client.query(&stmt, &[&article_id]).await?;
Transactions
let tx = client.transaction().await?;
tx.execute("INSERT INTO articles (id, title) VALUES ($1, $2)", &[&id, &title]).await?;
tx.execute("INSERT INTO article_versions (article_id, content) VALUES ($1, $2)", &[&id, &content]).await?;
tx.commit().await?;
// Or roll back:
// tx.rollback().await?;
The Transaction type implements GenericClient, so you can pass &tx anywhere that accepts &dyn GenericClient.
Type Mapping (with features enabled)
| Rust type (feature) | Postgres type | Feature flag |
|---|---|---|
&str / String | TEXT, VARCHAR | default |
i32 | INT4 | default |
i64 | INT8 | default |
f64 | FLOAT8 | default |
bool | BOOL | default |
uuid::Uuid | UUID | with-uuid-1 |
chrono::NaiveDateTime | TIMESTAMP | with-chrono-0_4 |
chrono::DateTime<Utc> | TIMESTAMPTZ | with-chrono-0_4 |
chrono::NaiveDate | DATE | with-chrono-0_4 |
serde_json::Value | JSON/JSONB | with-serde_json-1 |
&[u8] / Vec<u8> | BYTEA | default |
Nullable columns: use Option<T> (e.g., Option<&str>, Option<uuid::Uuid>).
Common Pitfalls
- Specifying parameter types: When a parameter type is ambiguous, cast in SQL:
$1::UUID,$1::TEXT, etc. - Connection must be spawned: If you don't
tokio::spawntheConnection, queries will hang forever. - Owned vs borrowed:
query()borrows params. ForStringvs&str,queryaccepts&StringviaDeref. Use&strslices when possible. - Neon cold starts: Neon serverless has cold-start latency on first connect. Consider connection pooling for production.
- No connection pooling builtin: tokio-postgres does not include a pool. For pooling, use
deadpool-postgresorbb8-postgresin production.
Project-Specific Notes
- DB URL is read from
DATABASE_URLenv var (Neon connection string). - Migrations use the
migscrate — seeAGENTS.md. - This project uses
uuid::Uuid(withv4feature) andchronofor timestamps, so enablewith-uuid-1andwith-chrono-0_4feature flags. - Always use parameterized queries — never interpolate values into SQL strings.