Skip to content

Instantly share code, notes, and snippets.

@lmmx
Last active August 28, 2025 20:37
Show Gist options
  • Save lmmx/3a712c79f55d84971addea08d4dee953 to your computer and use it in GitHub Desktop.
Save lmmx/3a712c79f55d84971addea08d4dee953 to your computer and use it in GitHub Desktop.
Diesel SQL compilation to string with variable substitution
#!/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