# Rust dan SQLx 2026: Query yang Diverifikasi Saat Kompilasi dan Pertanyaan Wawancara > Pelajari cara SQLx 0.9 memverifikasi query SQL saat kompilasi, konfigurasi sqlx.toml, dan persiapan wawancara teknis untuk posisi backend Rust. - Published: 2026-09-12 - Updated: 2026-09-12 - Author: Anthony Fillion-Maillet - Tags: rust, sqlx, database, postgresql, interview - Reading time: 12 min --- SQLx 0.9 mengubah cara pengembang Rust berinteraksi dengan database. Versi terbaru ini menangkap kesalahan SQL saat kompilasi, bukan saat aplikasi berjalan di produksi. Dirilis pada Mei 2026, SQLx 0.9 memperkenalkan sistem konfigurasi baru melalui sqlx.toml, fleksibilitas runtime, dan keamanan query yang lebih ketat melalui trait SqlSafeStr. > **Keunggulan SQLx** > > Berbeda dengan ORM yang menghasilkan SQL dari struct Rust, SQLx memverifikasi query SQL yang ditulis manual terhadap database aktif saat kompilasi. Kesalahan ketik pada nama kolom akan menggagalkan proses build, bukan menyebabkan error di produksi. ## Verifikasi Query Saat Kompilasi di SQLx 0.9 Macro [query!](https://docs.rs/sqlx/latest/sqlx/macro.query.html) memeriksa setiap statement SQL terhadap skema database selama `cargo build`. Fitur ini mendeteksi kesalahan ketik, ketidakcocokan tipe, dan kolom yang hilang sebelum kode dijalankan. ```rust // src/db/users.rs use sqlx::{FromRow, PgPool}; #[derive(FromRow)] pub struct User { pub id: i32, pub email: String, pub created_at: chrono::DateTime, } pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { // Diverifikasi saat kompilasi: nama kolom, tipe, dan struktur return sqlx::query_as!( User, r#" SELECT id, email, created_at FROM users WHERE email = $1 "#, email ) .fetch_optional(pool) .await } ``` Macro query_as! memetakan baris hasil langsung ke struct User. Jika tabel users tidak memiliki kolom created_at, kompilasi akan gagal dengan pesan error yang jelas menunjuk ke baris yang bermasalah. ## Sistem Konfigurasi sqlx.toml SQLx 0.9 memperkenalkan [file sqlx.toml](https://github.com/launchbadge/sqlx) yang memusatkan konfigurasi database, override tipe, dan pengaturan multi-database. Pendekatan ini menggantikan variabel environment yang tersebar dan menyederhanakan pipeline CI. ```toml # sqlx.toml [common] database_url_var = "DATABASE_URL" [macros] default_type_override.uuid = "uuid::Uuid" default_type_override.timestamptz = "chrono::DateTime" [sqlite] extensions = ["uuid", "crypto"] ``` Override tipe menghilangkan casting berulang. Setiap kolom UUID secara otomatis dipetakan ke uuid::Uuid, dan setiap timestamptz ke chrono::DateTime tanpa anotasi per-query. ## SqlSafeStr: Mencegah SQL Injection pada Level Tipe SQLx 0.9 memperkuat keamanan query dengan mewajibkan SqlSafeStr untuk parameter string. Secara default, hanya `&'static str` yang mengimplementasikan trait ini, memblokir string dinamis yang berpotensi membawa payload injection. ```rust // src/db/search.rs use sqlx::{AssertSqlSafe, PgPool}; // Nama tabel statis: langsung dikompilasi const TABLE: &str = "products"; pub async fn search_products( pool: &PgPool, user_query: &str, ) -> Result, sqlx::Error> { // Aman: $1 adalah nilai parameter, bukan SQL yang diinterpolasi sqlx::query_as!( Product, r#"SELECT * FROM products WHERE name ILIKE '%' || $1 || '%'"#, user_query ) .fetch_all(pool) .await } // Tabel dinamis dari config: bungkus dengan AssertSqlSafe setelah validasi pub async fn count_table(pool: &PgPool, table: &str) -> Result { // Validasi nama tabel terhadap allowlist sebelum assert safety let allowed = ["users", "products", "orders"]; if !allowed.contains(&table) { return Err(sqlx::Error::Configuration("Invalid table".into())); } let query = format!("SELECT COUNT(*) as count FROM {}", table); let row: (i64,) = sqlx::query_as(&AssertSqlSafe(query)) .fetch_one(pool) .await?; Ok(row.0) } ``` Input pengguna melewati parameterized query dengan placeholder $1. SQL dinamis dari sumber terpercaya memerlukan pembungkusan eksplisit dengan AssertSqlSafe setelah validasi. ## Migrasi Database dengan sqlx-cli Tool sqlx-cli mengelola migrasi skema dengan file SQL berversi. Migrasi dijalankan secara berurutan, dan tool ini melacak migrasi yang sudah diterapkan. ```bash # Install CLI (0.9.x tidak lagi memerlukan --locked) cargo install sqlx-cli --features postgres # Buat migrasi baru sqlx migrate add create_users_table # Jalankan migrasi pending sqlx migrate run # Cek status migrasi sqlx migrate info ``` Setiap migrasi membuat sepasang file di direktori migrations/: ```sql -- migrations/20260912120000_create_users_table.up.sql CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_users_email ON users(email); ``` ```sql -- migrations/20260912120000_create_users_table.down.sql DROP TABLE IF EXISTS users; ``` File up.sql dijalankan saat migrate run, dan down.sql saat migrate revert. Menyimpan kedua file memungkinkan rollback deployment yang gagal. ## Mode Offline untuk Pipeline CI Pemeriksaan saat kompilasi memerlukan koneksi database selama build. Untuk environment CI tanpa akses database, SQLx meng-cache metadata query di direktori .sqlx/. ```bash # Generate cache query secara lokal (memerlukan DATABASE_URL) cargo sqlx prepare # Commit direktori .sqlx/ git add .sqlx/ git commit -m "Update SQLx query cache" ``` Di CI, build menggunakan metadata yang di-cache: ```yaml # .github/workflows/ci.yml jobs: build: runs-on: ubuntu-latest env: SQLX_OFFLINE: true steps: - uses: actions/checkout@v4 - name: Build run: cargo build --release ``` Variabel environment SQLX_OFFLINE=true memberitahu SQLx untuk menggunakan metadata yang di-cache alih-alih terhubung ke database. Pendekatan ini menjaga CI tetap cepat dan menghindari kredensial database di environment build. ## Connection Pooling dengan PgPool SQLx menyediakan connection pooling bawaan. PgPool mengelola kumpulan koneksi, menggunakannya kembali di seluruh request alih-alih membuka koneksi baru setiap kali. ```rust // src/main.rs use sqlx::postgres::PgPoolOptions; use std::time::Duration; #[tokio::main] async fn main() -> Result<(), sqlx::Error> { let pool = PgPoolOptions::new() .max_connections(10) .min_connections(2) .acquire_timeout(Duration::from_secs(3)) .idle_timeout(Duration::from_secs(600)) .connect(&std::env::var("DATABASE_URL").expect("DATABASE_URL required")) .await?; // Pool adalah Clone dan Send, aman untuk dibagikan antar task let app_state = AppState { db: pool }; // Mulai web server dengan app_state... Ok(()) } ``` Atur max_connections berdasarkan batas koneksi database dan jumlah instance aplikasi. Aturan umum: database_max_connections / number_of_instances. ## Pertanyaan Wawancara: SQLx dan Akses Database di Rust Wawancara teknis untuk posisi backend Rust sering menggali pemahaman tentang penanganan database. Pertanyaan-pertanyaan ini menguji pemahaman tentang jaminan compile-time SQLx dan pola async. **T: Bagaimana SQLx memverifikasi query saat kompilasi?** Macro query! terhubung ke database yang berjalan selama cargo build. Macro ini mem-parsing SQL, mengirimkannya ke database untuk perencanaan (tanpa eksekusi), dan memverifikasi bahwa nama kolom ada, tipe cocok, dan query valid secara sintaks. Query planner database yang melakukan validasi, sehingga edge case spesifik Postgres, MySQL, atau SQLite dapat terdeteksi. **T: Apa yang terjadi jika skema database berubah setelah kompilasi?** Binary yang dikompilasi berisi query plan dari waktu build. Jika skema berubah (kolom diganti nama, tipe diubah), query akan gagal saat runtime dengan error decode atau column-not-found. Tim memitigasi ini dengan menjalankan migrasi sebelum deployment dan rebuild setelah perubahan skema. Di produksi, blue-green deployment memastikan binary baru dan skema baru diluncurkan bersamaan. **T: Kapan memilih SQLx dibanding Diesel atau SeaORM?** SQLx cocok untuk proyek di mana pengembang menginginkan kontrol penuh atas SQL tanpa mempelajari DSL. Diesel menghasilkan SQL dari kode Rust dan mendeteksi perubahan skema saat kompilasi melalui file schema.rs. SeaORM menyediakan API gaya ActiveRecord dengan migrasi. SQLx paling cocok ketika tim menulis SQL kompleks (CTE, window function, fitur spesifik database) dan menginginkan pemeriksaan compile-time tanpa abstraksi. **T: Jelaskan tradeoff antara query! dan query_as!** Macro query! mengembalikan tipe record anonim dengan field yang cocok dengan kolom SELECT. Macro query_as! memetakan hasil ke struct bernama yang mengimplementasikan FromRow. Gunakan query! untuk query sekali pakai di mana mendefinisikan struct menambah overhead. Gunakan query_as! ketika tipe hasil muncul di banyak tempat atau perlu mengimplementasikan trait. ```rust // query! mengembalikan record anonim let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id) .fetch_one(pool) .await?; let name: String = row.name; // Akses dengan nama field // query_as! memetakan ke struct let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", user_id) .fetch_one(pool) .await?; ``` **T: Bagaimana menangani transaksi di SQLx?** Mulai transaksi dengan pool.begin(), jalankan query pada objek transaksi, kemudian panggil commit() atau biarkan drop untuk rollback. ```rust // src/db/orders.rs pub async fn create_order_with_items( pool: &PgPool, order: NewOrder, items: Vec, ) -> Result { let mut tx = pool.begin().await?; let order_id: (i32,) = sqlx::query_as( "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id" ) .bind(order.user_id) .bind(order.total) .fetch_one(&mut *tx) .await?; for item in items { sqlx::query( "INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)" ) .bind(order_id.0) .bind(item.product_id) .bind(item.quantity) .execute(&mut *tx) .await?; } tx.commit().await?; Ok(order_id.0) } ``` Jika query gagal, transaksi otomatis rollback ketika tx di-drop. Rollback eksplisit jarang diperlukan. ## Pola Produksi untuk SQLx 0.9 Pola-pola ini menangani skenario produksi umum: menangani kolom nullable, tipe custom, dan operasi batch. ### Kolom Nullable dan Tipe Option ```rust #[derive(FromRow)] pub struct UserProfile { pub id: i32, pub email: String, pub avatar_url: Option, // Kolom nullable dipetakan ke Option pub bio: Option, } pub async fn update_profile( pool: &PgPool, user_id: i32, avatar_url: Option<&str>, bio: Option<&str>, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE users SET avatar_url = COALESCE($2, avatar_url), bio = COALESCE($3, bio), updated_at = NOW() WHERE id = $1 "#, user_id, avatar_url, bio ) .execute(pool) .await?; Ok(()) } ``` ### Batch Insert dengan UNNEST Untuk bulk insert, UNNEST menghindari multiple round trip: ```rust pub async fn insert_tags(pool: &PgPool, tags: &[String]) -> Result, sqlx::Error> { let ids: Vec<(i32,)> = sqlx::query_as( r#" INSERT INTO tags (name) SELECT * FROM UNNEST($1::text[]) ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id "# ) .bind(tags) .fetch_all(pool) .await?; Ok(ids.into_iter().map(|(id,)| id).collect()) } ``` ## Ringkasan SQLx di Rust - Macro query! memverifikasi SQL saat kompilasi terhadap database aktif, mendeteksi kesalahan ketik dan ketidakcocokan tipe sebelum runtime - SQLx 0.9 memperkenalkan sqlx.toml untuk override tipe, konfigurasi multi-database, dan loading ekstensi SQLite - SqlSafeStr mencegah SQL injection dengan membatasi string query ke nilai statis kecuali dibungkus eksplisit dengan AssertSqlSafe - Gunakan `cargo sqlx prepare` untuk meng-cache metadata query untuk build offline di CI - PgPool menangani connection pooling dengan limit, timeout, dan pengaturan idle yang dapat dikonfigurasi - Migrasi disimpan dalam file SQL yang dikelola oleh sqlx-cli, dengan pasangan up/down untuk dukungan rollback - Untuk persiapan wawancara Rust, latih penjelasan verifikasi compile-time, tradeoff query! vs query_as!, dan pola penanganan transaksi --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/rust/rust-sqlx-compile-time-queries-tutorial