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}