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/// What a `:compose(...)` target refers to.
57#[derive(Debug, Clone, PartialEq)]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59pub enum ComposeTarget {
60    /// A direct file path.
61    Path(PathBuf),
62    /// A slot reference (name without `@` prefix).
63    Slot(String),
64}
65
66/// A slot assignment: `@name = path`.
67#[derive(Debug, Clone, PartialEq)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69pub struct SlotAssignment {
70    /// The slot name (without `@` prefix).
71    pub name: String,
72    /// The file path to assign to this slot.
73    pub path: PathBuf,
74}
75
76/// A compose reference parsed from `:compose(target, @slot = path, ...)`.
77#[derive(Debug, Clone, PartialEq)]
78#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
79pub struct ComposeRef {
80    /// The target: either a file path or a slot reference.
81    pub target: ComposeTarget,
82    /// Slot assignments provided by the caller.
83    pub slots: Vec<SlotAssignment>,
84}
85
86/// An aggregate command parsed from `:count(...)` or `:union(...)`.
87#[derive(Debug, Clone, PartialEq)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89pub struct Command {
90    /// The kind of command (count or union).
91    pub kind: CommandKind,
92    /// Whether the DISTINCT modifier is present.
93    pub distinct: bool,
94    /// Whether the ALL modifier is present.
95    pub all: bool,
96    /// Optional column list (from `columns OF`).
97    pub columns: Option<Vec<String>>,
98    /// Source template paths.
99    pub sources: Vec<PathBuf>,
100}
101
102/// The kind of aggregate command.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105pub enum CommandKind {
106    /// COUNT command - wraps in `SELECT COUNT(*) FROM (...)`.
107    Count,
108    /// UNION command - combines sources with UNION.
109    Union,
110}
111
112/// Target database dialect for placeholder syntax.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
115pub enum Dialect {
116    /// PostgreSQL: `$1`, `$2`, `$3`
117    Postgres,
118    /// MySQL: `?`, `?`, `?`
119    Mysql,
120    /// SQLite: `?1`, `?2`, `?3`
121    Sqlite,
122}
123
124impl Dialect {
125    /// Format a placeholder for the given 1-based parameter index.
126    pub fn placeholder(&self, index: usize) -> String {
127        match self {
128            Dialect::Postgres => format!("${index}"),
129            Dialect::Mysql => "?".to_string(),
130            Dialect::Sqlite => format!("?{index}"),
131        }
132    }
133
134    /// Whether this dialect uses numbered placeholders ($1, ?1) vs positional (?).
135    ///
136    /// Numbered dialects (Postgres, SQLite) support alphabetical parameter ordering
137    /// and deduplication. Positional dialects (MySQL) use document-order placeholders.
138    pub fn supports_numbered_placeholders(&self) -> bool {
139        matches!(self, Dialect::Postgres | Dialect::Sqlite)
140    }
141}