Skip to main content

spawn_db/sql_formatter/
mod.rs

1//! SQL escaping formatters for minijinja templates.
2//!
3//! This crate provides SQL-safe value formatting for different database dialects.
4//! Each dialect module implements escaping rules appropriate for that database.
5//!
6//! # Supported Dialects
7//!
8//! - [`SqlDialect::Postgres`] - PostgreSQL escaping (works for psql CLI and native drivers)
9//!
10//! # Usage
11//!
12//! ```
13//! use spawn_db::sql_formatter::{SqlDialect, get_auto_escape_callback, get_formatter};
14//! use minijinja::Environment;
15//!
16//! let mut env = Environment::new();
17//! env.set_auto_escape_callback(get_auto_escape_callback(SqlDialect::Postgres));
18//! env.set_formatter(get_formatter(SqlDialect::Postgres));
19//! ```
20
21pub mod postgres;
22
23use minijinja::{AutoEscape, Output, State, Value};
24
25/// SQL dialect for formatting.
26///
27/// Different databases have different escaping rules and syntax for literals,
28/// identifiers, arrays, etc. This enum selects the appropriate formatter.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SqlDialect {
31    /// PostgreSQL dialect.
32    ///
33    /// Used for:
34    /// - `psql` CLI tool
35    /// - Native PostgreSQL drivers (e.g., `tokio-postgres`, `sqlx` with postgres)
36    /// - PostgreSQL-compatible databases (e.g., CockroachDB, YugabyteDB)
37    Postgres,
38    // Future dialects:
39    // MySQL,
40    // SqlServer,
41    // Sqlite,
42}
43
44impl SqlDialect {
45    /// Returns the auto-escape format name for this dialect.
46    ///
47    /// This is used with minijinja's `AutoEscape::Custom` to identify
48    /// which escaping mode is active.
49    pub fn format_name(&self) -> &'static str {
50        match self {
51            SqlDialect::Postgres => "sql-postgres",
52        }
53    }
54}
55
56/// Type alias for minijinja formatter functions.
57pub type FormatterFn = fn(&mut Output<'_>, &State<'_, '_>, &Value) -> Result<(), minijinja::Error>;
58
59/// Type alias for minijinja auto-escape callback functions.
60pub type AutoEscapeCallback = fn(&str) -> AutoEscape;
61
62/// Returns the appropriate formatter function for the given SQL dialect.
63///
64/// The returned function can be passed directly to `Environment::set_formatter()`.
65///
66/// # Example
67///
68/// ```
69/// use spawn_db::sql_formatter::{SqlDialect, get_formatter};
70/// use minijinja::Environment;
71///
72/// let mut env = Environment::new();
73/// let formatter = get_formatter(SqlDialect::Postgres);
74/// env.set_formatter(formatter);
75/// ```
76pub fn get_formatter(dialect: SqlDialect) -> FormatterFn {
77    match dialect {
78        SqlDialect::Postgres => postgres::sql_escape_formatter,
79    }
80}
81
82/// Returns an auto-escape callback for the given SQL dialect.
83///
84/// The callback enables SQL escaping for all files.
85/// This should be passed to `Environment::set_auto_escape_callback()`.
86///
87/// # Example
88///
89/// ```
90/// use spawn_db::sql_formatter::{SqlDialect, get_auto_escape_callback};
91/// use minijinja::Environment;
92///
93/// let mut env = Environment::new();
94/// let callback = get_auto_escape_callback(SqlDialect::Postgres);
95/// env.set_auto_escape_callback(callback);
96/// ```
97pub fn get_auto_escape_callback(dialect: SqlDialect) -> AutoEscapeCallback {
98    match dialect {
99        SqlDialect::Postgres => postgres::auto_escape_callback,
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_dialect_format_names_are_unique() {
109        // Ensure each dialect has a unique format name
110        let dialects = [SqlDialect::Postgres];
111        let names: Vec<_> = dialects.iter().map(|d| d.format_name()).collect();
112
113        for (i, name) in names.iter().enumerate() {
114            for (j, other) in names.iter().enumerate() {
115                if i != j {
116                    assert_ne!(name, other, "Dialect format names must be unique");
117                }
118            }
119        }
120    }
121
122    #[test]
123    fn test_get_formatter_returns_function() {
124        // Just verify we can get a formatter for each dialect
125        let _ = get_formatter(SqlDialect::Postgres);
126    }
127
128    #[test]
129    fn test_get_auto_escape_callback_returns_function() {
130        let callback = get_auto_escape_callback(SqlDialect::Postgres);
131
132        // Verify .sql files trigger custom escaping
133        match callback("test.sql") {
134            AutoEscape::Custom(name) => assert_eq!(name, "sql-postgres"),
135            _ => panic!("Expected Custom auto-escape for .sql files"),
136        }
137
138        // Verify non-.sql files also trigger escaping (all files use SQL escaping)
139        match callback("test.txt") {
140            AutoEscape::Custom(name) => assert_eq!(name, "sql-postgres"),
141            _ => panic!("Expected Custom auto-escape for .txt files"),
142        }
143    }
144}