1mod chunk;
2mod cte;
3mod owned;
4mod tokens;
5
6use crate::prelude::*;
7use crate::{
8 dialect::DialectExt,
9 param::{Param, ParamBind},
10 placeholder::Placeholder,
11 traits::{SQLColumnInfo, SQLParam, SQLTableInfo, ToSQL},
12};
13pub use chunk::*;
14use core::fmt::Display;
15pub use owned::*;
16use smallvec::SmallVec;
17pub use tokens::*;
18
19#[cfg(feature = "profiling")]
20use crate::profile_sql;
21
22#[derive(Debug, Clone)]
27pub struct SQL<'a, V: SQLParam> {
28 pub chunks: SmallVec<[SQLChunk<'a, V>; 8]>,
29}
30
31impl<'a, V: SQLParam> SQL<'a, V> {
32 const POSITIONAL_PLACEHOLDER: Placeholder = Placeholder::positional();
33
34 #[inline]
38 pub const fn empty() -> Self {
39 Self {
40 chunks: SmallVec::new_const(),
41 }
42 }
43
44 #[inline]
48 pub fn token(t: Token) -> Self {
49 Self {
50 chunks: smallvec::smallvec![SQLChunk::Token(t)],
51 }
52 }
53
54 #[inline]
56 pub fn with_capacity_chunks(capacity: usize) -> Self {
57 Self {
58 chunks: SmallVec::with_capacity(capacity),
59 }
60 }
61
62 #[inline]
64 pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
65 Self {
66 chunks: smallvec::smallvec![SQLChunk::Ident(name.into())],
67 }
68 }
69
70 #[inline]
72 pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
73 Self {
74 chunks: smallvec::smallvec![SQLChunk::Raw(text.into())],
75 }
76 }
77
78 #[inline]
80 pub fn param(value: impl Into<Cow<'a, V>>) -> Self {
81 Self {
82 chunks: smallvec::smallvec![SQLChunk::Param(Param {
83 value: Some(value.into()),
84 placeholder: Self::POSITIONAL_PLACEHOLDER,
85 })],
86 }
87 }
88
89 #[inline]
93 pub fn bytes(bytes: impl Into<Cow<'a, [u8]>>) -> Self
94 where
95 V: From<&'a [u8]>,
96 V: From<Vec<u8>>,
97 V: Into<Cow<'a, V>>,
98 {
99 match bytes.into() {
100 Cow::Borrowed(value) => Self::param(V::from(value)),
101 Cow::Owned(value) => Self::param(V::from(value)),
102 }
103 }
104
105 #[inline]
107 pub fn placeholder(name: &'static str) -> Self {
108 Self {
109 chunks: smallvec::smallvec![SQLChunk::Param(Param {
110 value: None,
111 placeholder: Placeholder::colon(name),
112 })],
113 }
114 }
115
116 #[inline]
118 pub fn table(table: &'static dyn SQLTableInfo) -> Self {
119 Self {
120 chunks: smallvec::smallvec![SQLChunk::Table(table)],
121 }
122 }
123
124 #[inline]
126 pub fn column(column: &'static dyn SQLColumnInfo) -> Self {
127 Self {
128 chunks: smallvec::smallvec![SQLChunk::Column(column)],
129 }
130 }
131
132 #[inline]
135 pub fn func(name: &'static str, args: SQL<'a, V>) -> Self {
136 let args = if args.is_subquery() {
137 args.parens()
138 } else {
139 args
140 };
141 SQL::raw(name)
142 .push(Token::LPAREN)
143 .append(args)
144 .push(Token::RPAREN)
145 }
146
147 #[inline]
151 pub fn append(mut self, other: impl Into<SQL<'a, V>>) -> Self {
152 #[cfg(feature = "profiling")]
153 profile_sql!("append");
154 let mut other = other.into();
155 if !other.chunks.is_empty() {
156 self.chunks.reserve(other.chunks.len());
157 self.chunks.extend(other.chunks.drain(..));
158 }
159 self
160 }
161
162 #[inline]
164 pub fn push(mut self, chunk: impl Into<SQLChunk<'a, V>>) -> Self {
165 self.chunks.push(chunk.into());
166 self
167 }
168
169 #[inline]
171 pub fn with_capacity(mut self, additional: usize) -> Self {
172 self.chunks.reserve(additional);
173 self
174 }
175
176 pub fn join<T>(sqls: T, separator: Token) -> SQL<'a, V>
180 where
181 T: IntoIterator,
182 T::Item: ToSQL<'a, V>,
183 {
184 #[cfg(feature = "profiling")]
185 profile_sql!("join");
186
187 let mut iter = sqls.into_iter();
188 let Some(first) = iter.next() else {
189 return SQL::empty();
190 };
191
192 let mut result = first.to_sql();
193 let (lower, _) = iter.size_hint();
194 if lower > 0 {
195 result.chunks.reserve(lower * 2);
197 }
198 for item in iter {
199 result = result.push(separator).append(item.to_sql());
200 }
201 result
202 }
203
204 #[inline]
206 pub fn parens(self) -> Self {
207 SQL::token(Token::LPAREN).append(self).push(Token::RPAREN)
208 }
209
210 #[inline]
212 pub fn is_subquery(&self) -> bool {
213 matches!(self.chunks.first(), Some(SQLChunk::Token(Token::SELECT)))
214 }
215
216 pub fn alias(self, name: impl Into<Cow<'a, str>>) -> SQL<'a, V> {
218 self.push(Token::AS).push(SQLChunk::Ident(name.into()))
219 }
220
221 pub fn param_list<I>(values: I) -> Self
223 where
224 I: IntoIterator,
225 I::Item: Into<Cow<'a, V>>,
226 {
227 Self::join(values.into_iter().map(Self::param), Token::COMMA)
228 }
229
230 pub fn assignments<I, T>(pairs: I) -> Self
232 where
233 I: IntoIterator<Item = (&'static str, T)>,
234 T: Into<Cow<'a, V>>,
235 {
236 Self::join(
237 pairs
238 .into_iter()
239 .map(|(col, val)| SQL::ident(col).push(Token::EQ).append(SQL::param(val))),
240 Token::COMMA,
241 )
242 }
243
244 pub fn into_owned(self) -> OwnedSQL<V> {
248 OwnedSQL::from(self)
249 }
250
251 pub fn sql(&self) -> String {
254 #[cfg(feature = "profiling")]
255 profile_sql!("sql");
256 let capacity = self.estimate_capacity();
257 let mut buf = String::with_capacity(capacity);
258 self.write_to(&mut buf);
259 buf
260 }
261
262 pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
266 use crate::dialect::Dialect;
267 let mut param_index = 1usize;
268 for (i, chunk) in self.chunks.iter().enumerate() {
269 match chunk {
270 SQLChunk::Token(Token::SELECT) => {
271 chunk.write(buf);
272 self.write_select_columns(buf, i);
273 }
274 SQLChunk::Param(param) => {
275 if let Some(name) = param.placeholder.name
278 && V::DIALECT == Dialect::SQLite
279 {
280 let _ = buf.write_char(':');
281 let _ = buf.write_str(name);
282 } else {
283 let _ = buf.write_str(&V::DIALECT.render_placeholder(param_index));
284 }
285 param_index += 1;
286 }
287 _ => chunk.write(buf),
288 }
289
290 if self.needs_space(i) {
291 let _ = buf.write_char(' ');
292 }
293 }
294 }
295
296 pub fn write_chunk_to(
298 &self,
299 buf: &mut impl core::fmt::Write,
300 chunk: &SQLChunk<'a, V>,
301 index: usize,
302 ) {
303 match chunk {
304 SQLChunk::Token(Token::SELECT) => {
305 chunk.write(buf);
306 self.write_select_columns(buf, index);
307 }
308 _ => chunk.write(buf),
309 }
310 }
311
312 fn write_select_columns(&self, buf: &mut impl core::fmt::Write, select_index: usize) {
314 let chunks = self.chunks.get(select_index + 1..select_index + 3);
315 match chunks {
316 Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(table)]) => {
317 let _ = buf.write_char(' ');
318 self.write_qualified_columns(buf, *table);
319 }
320 Some([SQLChunk::Token(Token::FROM), _]) => {
321 let _ = buf.write_char(' ');
322 let _ = buf.write_str(Token::STAR.as_str());
323 }
324 _ => {}
325 }
326 }
327
328 pub fn write_qualified_columns(
330 &self,
331 buf: &mut impl core::fmt::Write,
332 table: &dyn SQLTableInfo,
333 ) {
334 let columns = table.columns();
335 if columns.is_empty() {
336 let _ = buf.write_char('*');
337 return;
338 }
339
340 for (i, col) in columns.iter().enumerate() {
341 if i > 0 {
342 let _ = buf.write_str(", ");
343 }
344 let _ = buf.write_char('"');
345 let _ = buf.write_str(table.name());
346 let _ = buf.write_str("\".\"");
347 let _ = buf.write_str(col.name());
348 let _ = buf.write_char('"');
349 }
350 }
351
352 fn estimate_capacity(&self) -> usize {
353 const PLACEHOLDER_SIZE: usize = 2;
354 const IDENT_OVERHEAD: usize = 2;
355 const COLUMN_OVERHEAD: usize = 5;
356 const ALIAS_OVERHEAD: usize = 6;
357
358 self.chunks
359 .iter()
360 .map(|chunk| match chunk {
361 SQLChunk::Token(t) => t.as_str().len(),
362 SQLChunk::Ident(s) => s.len() + IDENT_OVERHEAD,
363 SQLChunk::Raw(s) => s.len(),
364 SQLChunk::Param { .. } => PLACEHOLDER_SIZE,
365 SQLChunk::Table(t) => t.name().len() + IDENT_OVERHEAD,
366 SQLChunk::Column(c) => c.table().name().len() + c.name().len() + COLUMN_OVERHEAD,
367 SQLChunk::Alias { inner, alias } => {
368 alias.len()
369 + ALIAS_OVERHEAD
370 + match inner.as_ref() {
371 SQLChunk::Ident(s) => s.len() + IDENT_OVERHEAD,
372 SQLChunk::Raw(s) => s.len(),
373 _ => 10,
374 }
375 }
376 })
377 .sum::<usize>()
378 + self.chunks.len()
379 }
380
381 fn needs_space(&self, index: usize) -> bool {
383 let Some(next) = self.chunks.get(index + 1) else {
384 return false;
385 };
386
387 let current = &self.chunks[index];
388 chunk_needs_space(current, next)
389 }
390
391 pub fn params(&self) -> impl Iterator<Item = &V> {
394 self.chunks.iter().filter_map(|chunk| {
395 if let SQLChunk::Param(Param {
396 value: Some(value), ..
397 }) = chunk
398 {
399 Some(value.as_ref())
400 } else {
401 None
402 }
403 })
404 }
405
406 pub fn bind<T: SQLParam + Into<V>>(
408 self,
409 params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
410 ) -> SQL<'a, V> {
411 #[cfg(feature = "profiling")]
412 profile_sql!("bind");
413
414 let param_map: HashMap<&str, V> = params
415 .into_iter()
416 .map(Into::into)
417 .map(|p| (p.name, p.value.into()))
418 .collect();
419
420 let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
421 .chunks
422 .into_iter()
423 .map(|chunk| match chunk {
424 SQLChunk::Param(mut param) => {
425 if let Some(name) = param.placeholder.name
426 && let Some(value) = param_map.get(name)
427 {
428 param.value = Some(Cow::Owned(value.clone()));
429 }
430 SQLChunk::Param(param)
431 }
432 other => other,
433 })
434 .collect();
435
436 SQL {
437 chunks: bound_chunks,
438 }
439 }
440}
441
442fn chunk_needs_space<V: SQLParam>(current: &SQLChunk<'_, V>, next: &SQLChunk<'_, V>) -> bool {
444 if let SQLChunk::Raw(text) = current
446 && text.ends_with(' ')
447 {
448 return false;
449 }
450
451 if let SQLChunk::Raw(text) = next
453 && text.starts_with(' ')
454 {
455 return false;
456 }
457
458 match (current, next) {
459 (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT)) => false,
461 (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
463 (SQLChunk::Token(Token::COMMA), _) => true,
465 (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
467 (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
469 (SQLChunk::Token(t), _) if t.is_operator() => true,
471 (_, SQLChunk::Token(t)) if t.is_operator() => true,
472 _ => current.is_word_like() && next.is_word_like(),
474 }
475}
476
477impl<'a, V: SQLParam> Default for SQL<'a, V> {
480 fn default() -> Self {
481 Self::empty()
482 }
483}
484
485impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
486 fn from(s: &'a str) -> Self {
487 SQL::raw(s)
488 }
489}
490
491impl<'a, V: SQLParam> From<Token> for SQL<'a, V> {
492 fn from(value: Token) -> Self {
493 SQL::token(value)
494 }
495}
496
497impl<'a, V: SQLParam + 'a> AsRef<SQL<'a, V>> for SQL<'a, V> {
498 fn as_ref(&self) -> &SQL<'a, V> {
499 self
500 }
501}
502
503impl<'a, V: SQLParam + core::fmt::Display> Display for SQL<'a, V> {
504 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
505 let params: Vec<_> = self.params().collect();
507 write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
508 }
509}
510
511impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
512 fn to_sql(&self) -> SQL<'a, V> {
513 self.clone()
514 }
515}
516
517impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
518where
519 SQLChunk<'a, V>: From<T>,
520{
521 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
522 let chunks = SmallVec::from_iter(iter.into_iter().map(SQLChunk::from));
523 Self { chunks }
524 }
525}
526
527impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
528 type Item = SQLChunk<'a, V>;
529 type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
530
531 fn into_iter(self) -> Self::IntoIter {
532 self.chunks.into_iter()
533 }
534}