Setup Turso in Rust

Nestero
Bismillahirrahmanirrahim

Apa itu Turso

Singkatnya Turso itu engine database SQL in-process yang compatible dengan SQLite. Turso sangat ideal untuk aplikasi modern karena mendukung fitur embedded replica (Local-First). Turso bisa membaca/menulis ke database lokal SQLite dengan sangat cepat, lalu melakukan sinkronisasi (push/pull) ke server cloud di background.

Berikut adalah catatan bagaimana melakukan setup state database, melakukan sinkronisasi, dan menjalankan operasi CRUD sederhana di Rust.

Mendefinisikan State Database

Karena Rust memiliki aturan kepemilikan (ownership) dan konkurensi yang ketat, koneksi database yang akan dipakai secara bersamaan oleh banyak fungsi (atau thread) harus dibungkus menggunakan Arc dan tokio::sync::Mutex.

use turso::Connection;
use turso::sync::Database;
use std::sync::Arc;
use tokio::sync::Mutex;

// DbState menyimpan koneksi dan instance database yang bisa dibagikan 
// (cloned) dengan aman ke berbagai request/task.
#[derive(Clone)]
pub struct DbState {
    pub conn: Arc<Mutex<Connection>>, 
    pub db: Arc<Mutex<Database>>,
}

/// Inisialisasi Database Embedded Replica
pub async fn init_db(local_db_path: &str, turso_url: String, turso_token: String) -> Result<DbState, String> {
    //  Build database instance
    let db = turso::sync::Builder::new_remote(local_db_path)
        .with_remote_url(turso_url)
        .with_auth_token(turso_token)
        .build()
        .await
        .map_err(|e| e.to_string())?;

    // Buat koneksi dari instance database tersebut
    let conn = db.connect().await.map_err(|e| e.to_string())?;

    // Tarik data awal (Pull) dari Turso Cloud ke lokal
    if let Err(e) = db.pull().await {
        eprintln!("[Offline Mode] Gagal menarik data awal dari Turso: {}", e);
    } else {
        println!("[Online Mode] Berhasil menarik data awal dari Turso.");
    }

    // Bungkus dengan Arc<Mutex<...>> agar thread-safe
    Ok(DbState { 
        conn: Arc::new(Mutex::new(conn)),
        db: Arc::new(Mutex::new(db))
    })
}

Fungsi Sinkronisasi Manual (Push & Pull)

Dalam pendekatan local-first, bekerja dengan data lokal terlebih dahulu. Untuk menyelaraskan data lokal dengan cloud, bisa dengan cara memanggil metode push() dan pull() pada instance Database.

/// Fungsi untuk melakukan sinkronisasi dua arah
pub async fn sync_db_cloud(db_state: &DbState) -> Result<String, String> {
    // Kunci mutex untuk mendapatkan akses ke instance database
    let db = db_state.db.lock().await; 
    
    // Push perubahan lokal ke cloud
    db.push().await.map_err(|e| format!("Gagal mengirim (push) data: {}", e))?;
    
    // Pull perubahan terbaru dari cloud ke lokal
    db.pull().await.map_err(|e| format!("Gagal menarik (pull) data: {}", e))?;
    
    Ok("Data berhasil disinkronisasi penuh".to_string())
}

Operasi CRUD (Contoh: Tabel Pelanggan)

use serde::{Serialize, Deserialize};
use turso::params;
use crate::db_set::DbState; // Sesuaikan dengan struktur folder

#[derive(Serialize, Deserialize, Debug)]
pub struct Pelanggan {
    pub id: Option<i64>,
    pub nama: String,
    pub telepon: String,
    pub alamat: String,
}

#[derive(Serialize, Deserialize)]
pub struct NewPelanggan {
    pub nama: String,
    pub telepon: String,
    pub alamat: String,
}

// CREATE
pub async fn new_pelanggan(db_state: &DbState, input: NewPelanggan) -> Result<String, String> {
    let conn = db_state.conn.lock().await;
    
    conn.execute(
        r#"
        INSERT INTO pelanggan (nama, telepon, alamat)
        VALUES (?1, ?2, ?3)
        "#,
        params![input.nama, input.telepon, input.alamat]
    )
    .await
    .map_err(|e| e.to_string())?;

    Ok("Pelanggan baru berhasil ditambahkan".to_string())
}

// READ
pub async fn list_pelanggan(db_state: &DbState) -> Result<Vec<Pelanggan>, String> {
    let conn = db_state.conn.lock().await;

    let mut rows = conn.query(
        "SELECT id, nama, telepon, alamat FROM pelanggan ORDER BY nama ASC", 
        ()
    )
    .await
    .map_err(|e| e.to_string())?;

    let mut pelanggans = Vec::new();

    while let Some(row) = rows.next().await.map_err(|e| e.to_string())? {
        pelanggans.push(Pelanggan {
            id: row.get(0).ok(),
            nama: row.get(1).unwrap_or_default(),
            telepon: row.get(2).unwrap_or_default(),
            alamat: row.get(3).unwrap_or_default(),
        });
    }

    Ok(pelanggans)
}

// UPDATE
pub async fn edit_pelanggan(db_state: &DbState, id: i64, input: NewPelanggan) -> Result<String, String> {
    let conn = db_state.conn.lock().await;

    let rows_affected = conn.execute(
        r#"
        UPDATE pelanggan 
        SET nama = ?1, telepon = ?2, alamat = ?3
        WHERE id = ?4
        "#,
        params![input.nama, input.telepon, input.alamat, id]
    )
    .await
    .map_err(|e| e.to_string())?;

    if rows_affected == 0 {
        return Err("Pelanggan tidak ditemukan".to_string());
    }

    Ok("Pelanggan berhasil diupdate".to_string())
}

// DELETE
pub async fn delete_pelanggan(db_state: &DbState, id: i64) -> Result<String, String> {
    let conn = db_state.conn.lock().await;

    let rows_affected = conn.execute(
        r#"DELETE FROM pelanggan WHERE id = ?1"#,
        params![id]
    )
    .await
    .map_err(|e| e.to_string())?;

    if rows_affected == 0 {
        return Err("Pelanggan tidak ditemukan".to_string());
    }

    Ok("Pelanggan berhasil dihapus".to_string())
}

Main

pub mod db_set;
pub mod pelanggan;

use db_set::{init_db, sync_db_cloud};
use pelanggan::{new_pelanggan, list_pelanggan, edit_pelanggan, delete_pelanggan, NewPelanggan};
use dotenvy::dotenv;
use std::env;

#[tokio::main]
async fn main() {
    // Muat variabel environment
    dotenv().ok(); 
    let turso_url = env::var("TURSO_SYNC_URL").expect("Variabel TURSO_SYNC_URL tidak ditemukan");
    let turso_token = env::var("TURSO_AUTH_TOKEN").expect("Variabel TURSO_AUTH_TOKEN tidak ditemukan");
    let local_db_path = "./app-local.db";

    // Inisialisasi Database
    let db_state = match init_db(local_db_path, turso_url, turso_token).await {
        Ok(state) => state,
        Err(e) => {
            eprintln!("Gagal menginisialisasi database: {}", e);
            return;
        }
    };

    // CREATE: new pelanggan
    let tambah_req = new_pelanggan(&db_state, NewPelanggan {
        nama: "Budi Santoso".to_string(),
        telepon: "08123456789".to_string(),
        alamat: "Jl. Merdeka No. 1".to_string(),
    }).await;
    println!("CREATE: {:?}", tambah_req);

    // READ: list pelanggan
    if let Ok(daftar) = list_pelanggan(&db_state).await {
        println!("{} pelanggan:", daftar.len());
        for p in daftar {
            println!("  - [{}] {} (Telp: {})", p.id.unwrap_or(0), p.nama, p.telepon);
        }
    }

    // UPDATE: ID 1
    let update_req = edit_pelanggan(&db_state, 1, NewPelanggan {
        nama: "Budi Santoso (Updated)".to_string(),
        telepon: "08999999999".to_string(),
        alamat: "Jl. Merdeka No. 1A".to_string(),
    }).await;
    println!("UPDATE: {:?}", update_req);

    // DELETE: Menghapus pelanggan dengan ID 1
    // let delete_req = delete_pelanggan(&db_state, 1).await;
    // println!("DELETE: {:?}", delete_req);

    
    // SYNC
    let sync_status = sync_db_cloud(&db_state).await;
    println!("SYNC: {:?}", sync_status);
}
Sesungguhnya yang menyebabkan ilmu hilang adalah lupa dan tidak mengulanginya.

"Sesungguhnya yang menyebabkan ilmu hilang adalah lupa dan tidak mengulanginya."

Imam Az-Zuhri rahimahullah

Tags:

Referensi:

Catatan Terkait:

NESTECH ID

Copyright 2026. All rights reserved.