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}