Skip to main content

sql_composer/
types.rs

1//! Core types for the sql-composer template AST.
2
3use std::path::PathBuf;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// A parsed template consisting of a sequence of literal SQL and macro invocations.
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct Template {
12    /// The ordered elements that make up this template.
13    pub elements: Vec<Element>,
14    /// Where this template originated from.
15    pub source: TemplateSource,
16}
17
18/// The origin of a template.
19#[derive(Debug, Clone, PartialEq)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub enum TemplateSource {
22    /// Loaded from a file at the given path.
23    File(PathBuf),
24    /// Parsed from an inline string literal.
25    Literal(String),
26}
27
28/// A single element in a template.
29#[derive(Debug, Clone, PartialEq)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31pub enum Element {
32    /// Raw SQL text passed through unchanged.
33    Sql(String),
34    /// `:bind(name ...)` - a parameter placeholder.
35    Bind(Binding),
36    /// `:compose(path)` - include another template.
37    Compose(ComposeRef),
38    /// `:count(...)` or `:union(...)` - an aggregate command.
39    Command(Command),
40}
41
42/// A parameter binding parsed from `:bind(name ...)`.
43#[derive(Debug, Clone, PartialEq)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub struct Binding {
46    /// The name of the bind parameter.
47    pub name: String,
48    /// Minimum number of values expected (from `EXPECTING min`).
49    pub min_values: Option<u32>,
50    /// Maximum number of values expected (from `EXPECTING min..max`).
51    pub max_values: Option<u32>,
52    /// Whether this binding accepts NULL (from `NULL` keyword).
53    pub nullable: bool,
54}
55
56/// A compose reference parsed from `:compose(path)`.
57#[derive(Debug, Clone, PartialEq)]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59pub struct ComposeRef {
60    /// The path to the template to include.
61    pub path: PathBuf,
62}
63
64/// An aggregate command parsed from `:count(...)` or `:union(...)`.
65#[derive(Debug, Clone, PartialEq)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub struct Command {
68    /// The kind of command (count or union).
69    pub kind: CommandKind,
70    /// Whether the DISTINCT modifier is present.
71    pub distinct: bool,
72    /// Whether the ALL modifier is present.
73    pub all: bool,
74    /// Optional column list (from `columns OF`).
75    pub columns: Option<Vec<String>>,
76    /// Source template paths.
77    pub sources: Vec<PathBuf>,
78}
79
80/// The kind of aggregate command.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub enum CommandKind {
84    /// COUNT command - wraps in `SELECT COUNT(*) FROM (...)`.
85    Count,
86    /// UNION command - combines sources with UNION.
87    Union,
88}
89
90/// Target database dialect for placeholder syntax.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
93pub enum Dialect {
94    /// PostgreSQL: `$1`, `$2`, `$3`
95    Postgres,
96    /// MySQL: `?`, `?`, `?`
97    Mysql,
98    /// SQLite: `?1`, `?2`, `?3`
99    Sqlite,
100}
101
102impl Dialect {
103    /// Format a placeholder for the given 1-based parameter index.
104    pub fn placeholder(&self, index: usize) -> String {
105        match self {
106            Dialect::Postgres => format!("${index}"),
107            Dialect::Mysql => "?".to_string(),
108            Dialect::Sqlite => format!("?{index}"),
109        }
110    }
111
112    /// Whether this dialect uses numbered placeholders ($1, ?1) vs positional (?).
113    ///
114    /// Numbered dialects (Postgres, SQLite) support alphabetical parameter ordering
115    /// and deduplication. Positional dialects (MySQL) use document-order placeholders.
116    pub fn supports_numbered_placeholders(&self) -> bool {
117        matches!(self, Dialect::Postgres | Dialect::Sqlite)
118    }
119}