1use strid::*;
7
8mod expr;
9pub use expr::*;
10
11mod render;
12pub use render::*;
13
14mod stmt;
15pub use stmt::*;
16
17#[derive(Debug, Clone)]
19pub struct RenderedSql {
20 pub sql: String,
22
23 pub params: Vec<ParamName>,
25}
26
27#[braid]
31pub struct TableName;
32
33#[braid]
37pub struct ColumnName;
38
39#[braid]
44pub struct ParamName;
45
46#[braid]
50pub struct PgType;
51
52pub struct Lit<T: AsRef<str>>(pub T);
63
64impl<T: AsRef<str>> std::fmt::Display for Lit<T> {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "'")?;
67 for c in self.0.as_ref().chars() {
68 if c == '\'' {
69 write!(f, "''")?;
70 } else {
71 write!(f, "{}", c)?;
72 }
73 }
74 write!(f, "'")
75 }
76}
77
78pub struct Ident<T: AsRef<str>>(pub T);
89
90impl<T: AsRef<str>> std::fmt::Display for Ident<T> {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(f, "\"")?;
93 for c in self.0.as_ref().chars() {
94 if c == '"' {
95 write!(f, "\"\"")?;
96 } else {
97 write!(f, "{}", c)?;
98 }
99 }
100 write!(f, "\"")
101 }
102}
103
104pub fn escape_string(s: &str) -> String {
106 format!("{}", Lit(s))
107}
108
109pub fn quote_ident(name: &str) -> String {
114 format!("{}", Ident(name))
115}
116
117pub fn index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
121 let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
122 format!("idx_{}_{}", table, cols.join("_"))
123}
124
125pub fn unique_index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
129 let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
130 format!("uq_{}_{}", table, cols.join("_"))
131}
132
133pub fn check_constraint_name(table: &str, expr: &str) -> String {
138 let normalized = normalize_sql_expr_for_hash(expr);
139 let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
140 let suffix = &hex[..16];
141
142 const PG_IDENT_MAX: usize = 63;
143 let prefix_overhead = "ck__".len(); let suffix_len = suffix.len();
145 let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
146
147 let table_part = if table.len() <= max_table_len {
148 table
149 } else {
150 let mut len = max_table_len.min(table.len());
152 while len > 0 && !table.is_char_boundary(len) {
153 len -= 1;
154 }
155 &table[..len]
156 };
157
158 format!("ck_{}_{}", table_part, suffix)
159}
160
161pub fn trigger_check_name(table: &str, expr: &str) -> String {
166 let normalized = normalize_sql_expr_for_hash(expr);
167 let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
168 let suffix = &hex[..16];
169
170 const PG_IDENT_MAX: usize = 63;
171 let prefix_overhead = "trgck__".len(); let suffix_len = suffix.len();
173 let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
174
175 let table_part = if table.len() <= max_table_len {
176 table
177 } else {
178 let mut len = max_table_len.min(table.len());
179 while len > 0 && !table.is_char_boundary(len) {
180 len -= 1;
181 }
182 &table[..len]
183 };
184
185 format!("trgck_{}_{}", table_part, suffix)
186}
187
188pub fn trigger_check_function_name(trigger_name: &str) -> String {
193 let hex = blake3::hash(trigger_name.as_bytes()).to_hex().to_string();
194 format!("trgfn_{}", &hex[..20])
195}
196
197fn normalize_sql_expr_for_hash(expr: &str) -> String {
198 let mut out = String::with_capacity(expr.len());
199 let mut pending_space = false;
200
201 let mut in_single_quote = false;
202 let mut in_double_quote = false;
203
204 let mut chars = expr.chars().peekable();
205 while let Some(ch) = chars.next() {
206 if in_single_quote {
207 out.push(ch);
208 if ch == '\'' {
209 if matches!(chars.peek(), Some('\'')) {
211 out.push(chars.next().expect("peeked"));
212 } else {
213 in_single_quote = false;
214 }
215 }
216 continue;
217 }
218
219 if in_double_quote {
220 out.push(ch);
221 if ch == '"' {
222 if matches!(chars.peek(), Some('"')) {
224 out.push(chars.next().expect("peeked"));
225 } else {
226 in_double_quote = false;
227 }
228 }
229 continue;
230 }
231
232 match ch {
233 '\'' => {
234 if pending_space && !out.is_empty() {
235 out.push(' ');
236 }
237 pending_space = false;
238 out.push('\'');
239 in_single_quote = true;
240 }
241 '"' => {
242 if pending_space && !out.is_empty() {
243 out.push(' ');
244 }
245 pending_space = false;
246 out.push('"');
247 in_double_quote = true;
248 }
249 c if c.is_whitespace() => {
250 pending_space = true;
251 }
252 c => {
253 if pending_space && !out.is_empty() {
254 out.push(' ');
255 }
256 pending_space = false;
257 out.push(c);
258 }
259 }
260 }
261
262 out.trim().to_string()
263}