A Rust framework for storing SQL in Markdown files with dynamic SQL and parameter binding support.
- π Markdown SQL: Write SQL in Markdown code blocks for better readability
- π Safe: Compile-time SQL injection checks, all parameters go through binding
- π¨ Dynamic SQL: MiniJinja template syntax with conditions and loops
- π SQL Reuse:
{% include %}to reference other SQL fragments - π High Performance: Templates pre-compiled at startup, zero parsing overhead at runtime
- π― Trait-based: Define trait interfaces, macros generate implementations
- π Transaction Support: Manual and closure-based transactions
- π¦ Batch Operations: One SQL + multiple parameter sets, prepared statement reuse
[dependencies]
markdown-sql = { git = "https://github.com/VonChange/markdown-sql.git", branch = "main" }
markdown-sql-macros = { git = "https://github.com/VonChange/markdown-sql.git", branch = "main" }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }sql/UserRepository.md:
# User Repository SQL
## Common Columns
β```sql
-- columns
id, name, age, status, created_at
β```
## Find User by ID
β```sql
-- findById
SELECT {% include "columns" %}
FROM users
WHERE id = #{id}
β```
## Conditional Query
β```sql
-- findByCondition
SELECT {% include "columns" %}
FROM users
WHERE 1=1
{% if name %}AND name LIKE #{name}{% endif %}
{% if status %}AND status = #{status}{% endif %}
β```
## Insert User
β```sql
-- insert
INSERT INTO users (name, age, status)
VALUES (#{name}, #{age}, #{status})
β```use markdown_sql_macros::repository;
use serde::Serialize;
// Parameter structs
#[derive(Serialize)]
pub struct IdParams {
pub id: i64,
}
#[derive(Serialize)]
pub struct ConditionParams {
pub name: Option<String>,
pub status: Option<i32>,
}
// Define Repository trait
// Method names auto-map to SQL IDs (snake_case β camelCase)
// find_by_id β findById
#[repository(sql_file = "sql/UserRepository.md")]
pub trait UserRepository {
async fn find_by_id(
&self,
db: &impl DbPool,
params: &IdParams,
) -> Result<Option<User>, AppError>;
async fn find_by_condition(
&self,
db: &impl DbPool,
params: &ConditionParams,
) -> Result<Vec<User>, AppError>;
async fn get_count(&self, db: &impl DbPool) -> Result<i64, AppError>;
async fn insert(
&self,
db: &impl DbPool,
user: &User,
) -> Result<u64, AppError>;
}use include_dir::{include_dir, Dir};
use markdown_sql::{DbPool, DbType, SqlManager};
use once_cell::sync::Lazy;
// Embed SQL directory
static SQL_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/sql");
// Global SQL manager
static SQL_MANAGER: Lazy<SqlManager> = Lazy::new(|| {
let mut manager = SqlManager::builder()
.db_type(DbType::Sqlite)
.debug(true)
.build()
.expect("Failed to create SqlManager");
manager
.load_embedded_dir(&SQL_DIR)
.expect("Failed to load SQL directory");
manager
});
pub fn get_user_repo() -> UserRepositoryImpl {
UserRepositoryImpl::new(&*SQL_MANAGER)
}
async fn example(db: &impl DbPool) {
let repo = get_user_repo();
// Query single
let user = repo.find_by_id(db, &IdParams { id: 1 }).await?;
// Conditional query
let users = repo.find_by_condition(db, &ConditionParams {
name: Some("%John%".to_string()),
status: None,
}).await?;
// Get count
let count = repo.get_count(db).await?;
// Insert
let affected = repo.insert(db, &new_user).await?;
}use markdown_sql::{begin_transaction, execute_tx, query_list_tx};
// Begin transaction
let mut tx = begin_transaction(&db).await?;
// Execute operations in transaction
execute_tx(&manager, &mut tx, "insert", &user1).await?;
execute_tx(&manager, &mut tx, "insert", &user2).await?;
// Query in transaction
let users: Vec<User> = query_list_tx(&manager, &mut tx, "findAll", &json!({})).await?;
// Commit
tx.commit().await?;
// If commit() is not called, transaction auto-rollbacks on dropuse markdown_sql::with_transaction;
with_transaction(&db, |tx| Box::pin(async move {
execute_tx(&manager, tx, "insert", &user1).await?;
execute_tx(&manager, tx, "update", &user2).await?;
Ok(())
})).await?;
// Auto-commit on success, auto-rollback on failureOne SQL + multiple parameter sets, prepared statement reuse, executed in transaction:
use markdown_sql::batch_execute;
let users = vec![
UserInsert { name: "User1".into(), age: 25, status: 1 },
UserInsert { name: "User2".into(), age: 30, status: 1 },
UserInsert { name: "User3".into(), age: 28, status: 1 },
];
// Batch insert (auto-transaction internally)
let affected = batch_execute(&manager, &db, "insert", &users).await?;
println!("Inserted {} rows", affected);use markdown_sql::{begin_transaction, batch_execute_tx};
let mut tx = begin_transaction(&db).await?;
batch_execute_tx(&manager, &mut tx, "insertUser", &users).await?;
batch_execute_tx(&manager, &mut tx, "updateOrder", &orders).await?;
tx.commit().await?;All Repository method db parameters accept types implementing DbPool trait:
use markdown_sql::DbPool;
pub struct AppDb {
pub sqlite: Pool<Sqlite>,
}
impl DbPool for AppDb {
fn pool(&self) -> &Pool<Sqlite> {
&self.sqlite
}
}
// Use &db directly, no need for &db.sqlite
repo.find_by_id(&db, ¶ms).await?;Built-in implementations:
Pool<Sqlite>&Pool<Sqlite>Arc<T>where T: DbPool
-- Use #{param} syntax, auto-converts to ? (SQLite/MySQL) or $1 (PostgreSQL)
SELECT * FROM users WHERE id = #{id} AND name = #{name}-- Conditionals
{% if name %}AND name = #{name}{% endif %}
-- Loops
{% for status in statuses %}
#{status}{% if not loop.last %},{% endif %}
{% endfor %}-- Define fragment
-- columns
id, name, age, status
-- Reference fragment
SELECT {% include "columns" %} FROM users-- Use bind_join filter for safe list expansion
WHERE id IN ({{ ids | bind_join(",") }})Compile-time detection of unsafe SQL patterns:
| Syntax | Status | Description |
|---|---|---|
#{param} |
β Safe | Parameter binding |
{{ list | bind_join(",") }} |
β Safe | IN queries |
{% if %} / {% for %} |
β Safe | Dynamic logic |
{{ param }} |
β Compile error | SQL injection risk |
{{ list | join(",") }} |
β Compile error | SQL injection risk |
{{ param | raw_safe }} |
Explicitly declared safe |
| Return Type | Execution | Description |
|---|---|---|
Vec<T> |
fetch_all | Query list |
Option<T> |
fetch_optional | Query single (optional) |
T |
fetch_one | Query single (required) |
i64 |
Scalar query | e.g., COUNT |
u64 |
execute | INSERT/UPDATE/DELETE affected rows |
This framework is designed with AI-assisted programming in mind:
| Traditional | markdown-sql |
|---|---|
| SQL embedded in code, AI struggles with context | SQL in Markdown, clear structure with comments |
| Magic strings scattered everywhere | Centralized SQL management, documented |
| SQL-business relationship unclear | Markdown headings describe intent |
- Clear Context: SQL blocks have descriptive headings
- Self-Documenting: AI understands each SQL's purpose from Markdown structure
- Easy Generation: AI can generate new SQL blocks following existing patterns
- Safe by Default:
#{param}syntax prevents AI from accidentally generating SQL injection vulnerabilities - Trait-based: AI only defines interfaces, no execution code needed
See detailed design document: plan/2025-12-21-markdown-sql.md
MIT