Last active
August 28, 2025 20:37
-
-
Save lmmx/3a712c79f55d84971addea08d4dee953 to your computer and use it in GitHub Desktop.
Diesel SQL compilation to string with variable substitution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env rust-script | |
//! ```cargo | |
//! [dependencies] | |
//! diesel = { version = "2.2", features = ["sqlite"] } | |
//! diesel-dynamic-schema = "0.2" | |
//! rusqlite = "0.31" | |
//! serde = { version = "1", features = ["derive"] } | |
//! serde_json = "1" | |
//! ``` | |
use diesel::prelude::*; | |
use diesel::sqlite::Sqlite; | |
use diesel::debug_query; | |
use diesel_dynamic_schema::table; | |
use rusqlite::{ | |
types::{ToSql, ToSqlOutput, Null}, | |
Connection, Result, | |
}; | |
use serde::Deserialize; | |
// Supported bind types (all ToSql-able) | |
#[derive(Debug, Deserialize)] | |
#[serde(untagged)] | |
enum BindValue { | |
Str(String), | |
Int(i64), | |
Float(f64), | |
Bool(bool), | |
Null, | |
} | |
// Make BindValue usable directly with rusqlite | |
impl ToSql for BindValue { | |
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> { | |
match self { | |
BindValue::Str(s) => Ok(ToSqlOutput::from(s.as_str())), | |
BindValue::Int(n) => Ok(ToSqlOutput::from(*n)), | |
BindValue::Float(f) => Ok(ToSqlOutput::from(*f)), | |
BindValue::Bool(b) => Ok(ToSqlOutput::from(*b)), | |
BindValue::Null => Ok(ToSqlOutput::from(Null)), | |
} | |
} | |
} | |
trait ExpandedSql { | |
fn expanded_sql(&self, conn: &Connection) -> Result<String>; | |
} | |
impl<Q> ExpandedSql for Q | |
where | |
Q: diesel::query_builder::QueryFragment<Sqlite> + diesel::query_builder::QueryId, | |
{ | |
fn expanded_sql(&self, conn: &Connection) -> Result<String> { | |
// Get SQL text and bind values from Diesel's debug_query | |
let sql_debug = debug_query::<Sqlite, _>(&self).to_string(); | |
let (sql_with_placeholders, binds_part) = sql_debug | |
.split_once("-- binds:") | |
.map(|(sql, binds)| (sql.trim(), binds)) | |
.unwrap(); | |
// Parse binds into Vec<BindValue> | |
let binds: Vec<BindValue> = serde_json::from_str(binds_part.trim()) | |
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( | |
0, | |
rusqlite::types::Type::Text, | |
Box::new(e), | |
))?; | |
// Prepare + bind automatically | |
let mut stmt = conn.prepare(sql_with_placeholders)?; | |
for (i, val) in binds.iter().enumerate() { | |
stmt.raw_bind_parameter(i + 1, val)?; | |
} | |
// Return expanded SQL with inlined values | |
Ok(stmt | |
.expanded_sql() | |
.unwrap_or_else(|| sql_with_placeholders.to_string())) | |
} | |
} | |
fn main() -> Result<()> { | |
// Demo DB setup belongs HERE, not inside the trait | |
let conn = Connection::open("demo.sqlite")?; | |
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, data TEXT);", [])?; | |
let users = table("users"); | |
let id_col = users.column::<diesel::sql_types::Integer, _>("id"); | |
let data_col = users.column::<diesel::sql_types::Text, _>("data"); | |
let query = users | |
.select((id_col, data_col)) | |
.filter(data_col.eq("Alice")) | |
.filter(id_col.eq(42)); | |
// ✅ pass in the connection | |
let expanded = query.expanded_sql(&conn)?; | |
println!("Expanded SQL:\n{expanded}"); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment