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}