1use crate::SQLConstraintKind;
2use crate::prelude::*;
3use crate::{Dialect, Param, Placeholder, SQLParam, sql::tokens::Token};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct EnumVariantRef {
13 pub label: &'static str,
15 pub discriminant: i64,
17}
18
19pub trait SQLEnumVariants {
26 const VARIANTS: &'static [EnumVariantRef];
28
29 const DEFAULT_LITERALS: &'static [&'static str] = &[];
33}
34
35#[must_use]
45pub const fn enum_default_literal<T: SQLEnumVariants>(label: &str) -> &'static str {
46 let mut index = 0;
47 while index < T::VARIANTS.len() {
48 if str_eq(T::VARIANTS[index].label, label) && index < T::DEFAULT_LITERALS.len() {
49 return T::DEFAULT_LITERALS[index];
50 }
51 index += 1;
52 }
53 panic!("the column default is not a variant of its enum type")
54}
55
56const fn str_eq(left: &str, right: &str) -> bool {
57 let (left, right) = (left.as_bytes(), right.as_bytes());
58 if left.len() != right.len() {
59 return false;
60 }
61 let mut index = 0;
62 while index < left.len() {
63 if left[index] != right[index] {
64 return false;
65 }
66 index += 1;
67 }
68 true
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum ColumnDialect {
77 SQLite {
78 autoincrement: bool,
79 default: Option<&'static str>,
80 generated_expression: Option<&'static str>,
81 generated_stored: bool,
82 collate: Option<&'static str>,
83 enum_variants: Option<&'static [EnumVariantRef]>,
85 },
86 PostgreSQL {
87 postgres_type: &'static str,
88 dimensions: Option<i32>,
89 is_serial: bool,
90 is_bigserial: bool,
91 is_generated_identity: bool,
92 is_identity_always: bool,
93 default: Option<&'static str>,
94 generated_expression: Option<&'static str>,
95 generated_stored: bool,
96 collate: Option<&'static str>,
97 comment: Option<&'static str>,
98 enum_variants: Option<&'static [EnumVariantRef]>,
100 },
101 MySQL {
102 auto_increment: bool,
103 default: Option<&'static str>,
104 generated_expression: Option<&'static str>,
105 generated_stored: bool,
106 charset: Option<&'static str>,
107 collate: Option<&'static str>,
108 on_update: Option<&'static str>,
109 },
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum TableDialect {
117 PostgreSQL {
118 is_unlogged: bool,
119 is_temporary: bool,
120 inherits: Option<&'static str>,
121 tablespace: Option<&'static str>,
122 is_rls_enabled: bool,
123 comment: Option<&'static str>,
124 },
125 SQLite {
126 without_rowid: bool,
127 strict: bool,
128 },
129 MySQL {
130 is_temporary: bool,
131 engine: Option<&'static str>,
132 charset: Option<&'static str>,
133 collate: Option<&'static str>,
134 comment: Option<&'static str>,
135 },
136}
137
138impl Default for TableDialect {
139 fn default() -> Self {
140 Self::PostgreSQL {
141 is_unlogged: false,
142 is_temporary: false,
143 inherits: None,
144 tablespace: None,
145 is_rls_enabled: false,
146 comment: None,
147 }
148 }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub struct ForeignKeyRef {
157 pub name: &'static str,
158 pub name_explicit: bool,
159 pub target_table: &'static str,
160 pub target_schema: &'static str,
161 pub source_columns: &'static [&'static str],
162 pub target_columns: &'static [&'static str],
163 pub on_delete: Option<&'static str>,
164 pub on_update: Option<&'static str>,
165 pub deferrable: bool,
166 pub initially_deferred: bool,
167}
168
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct PrimaryKeyRef {
172 pub columns: &'static [&'static str],
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct ConstraintRef {
178 pub name: Option<&'static str>,
179 pub name_explicit: bool,
180 pub kind: SQLConstraintKind,
181 pub columns: &'static [&'static str],
182 pub check_expression: Option<&'static str>,
183 pub deferrable: bool,
184 pub initially_deferred: bool,
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct TableRef {
196 pub name: &'static str,
198 pub column_names: &'static [&'static str],
199
200 pub schema: Option<&'static str>,
202 pub qualified_name: &'static str,
203 pub columns: &'static [ColumnRef],
204 pub primary_key: Option<PrimaryKeyRef>,
205 pub foreign_keys: &'static [ForeignKeyRef],
206 pub constraints: &'static [ConstraintRef],
207 pub dependency_names: &'static [&'static str],
208
209 pub dialect: TableDialect,
211}
212
213impl TableRef {
214 #[must_use]
220 pub const fn sql(name: &'static str, column_names: &'static [&'static str]) -> Self {
221 Self {
222 name,
223 column_names,
224 schema: None,
225 qualified_name: "",
226 columns: &[],
227 primary_key: None,
228 foreign_keys: &[],
229 constraints: &[],
230 dependency_names: &[],
231 dialect: TableDialect::PostgreSQL {
232 is_unlogged: false,
233 is_temporary: false,
234 inherits: None,
235 tablespace: None,
236 is_rls_enabled: false,
237 comment: None,
238 },
239 }
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub struct TableSqlRef {
246 pub schema: Option<&'static str>,
247 pub name: &'static str,
248 pub column_names: &'static [&'static str],
249}
250
251impl TableSqlRef {
252 #[inline]
254 #[must_use]
255 pub const fn from_table_ref(table: TableRef) -> Self {
256 Self {
257 schema: table.schema,
258 name: table.name,
259 column_names: table.column_names,
260 }
261 }
262
263 #[inline]
265 #[must_use]
266 pub const fn from_table_ref_ref(table: &TableRef) -> Self {
267 Self {
268 schema: table.schema,
269 name: table.name,
270 column_names: table.column_names,
271 }
272 }
273}
274
275impl From<&TableRef> for TableSqlRef {
276 #[inline]
277 fn from(value: &TableRef) -> Self {
278 Self::from_table_ref_ref(value)
279 }
280}
281
282impl From<TableRef> for TableSqlRef {
283 #[inline]
284 fn from(value: TableRef) -> Self {
285 Self::from_table_ref(value)
286 }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
303pub struct ColumnFlags(u8);
304
305impl ColumnFlags {
306 pub const NOT_NULL: Self = Self(1 << 0);
308 pub const PRIMARY_KEY: Self = Self(1 << 1);
310 pub const UNIQUE: Self = Self(1 << 2);
312 pub const HAS_DEFAULT: Self = Self(1 << 3);
314
315 #[must_use]
317 pub const fn empty() -> Self {
318 Self(0)
319 }
320
321 #[must_use]
323 pub const fn from_bits(bits: u8) -> Self {
324 Self(bits)
325 }
326
327 #[must_use]
329 pub const fn bits(self) -> u8 {
330 self.0
331 }
332
333 #[must_use]
335 pub const fn contains(self, other: Self) -> bool {
336 (self.0 & other.0) == other.0
337 }
338
339 #[must_use]
341 pub const fn union(self, other: Self) -> Self {
342 Self(self.0 | other.0)
343 }
344}
345
346impl core::ops::BitOr for ColumnFlags {
347 type Output = Self;
348 fn bitor(self, rhs: Self) -> Self {
349 self.union(rhs)
350 }
351}
352
353impl core::ops::BitOrAssign for ColumnFlags {
354 fn bitor_assign(&mut self, rhs: Self) {
355 *self = self.union(rhs);
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub struct ColumnRef {
366 pub table: &'static str,
368 pub name: &'static str,
369
370 pub sql_type: &'static str,
372 pub flags: ColumnFlags,
373
374 pub dialect: ColumnDialect,
376}
377
378impl ColumnRef {
379 #[must_use]
385 pub const fn sql(table: &'static str, name: &'static str) -> Self {
386 Self {
387 table,
388 name,
389 sql_type: "",
390 flags: ColumnFlags::empty(),
391 dialect: ColumnDialect::SQLite {
392 autoincrement: false,
393 default: None,
394 generated_expression: None,
395 generated_stored: false,
396 collate: None,
397 enum_variants: None,
398 },
399 }
400 }
401
402 #[must_use]
404 pub const fn not_null(&self) -> bool {
405 self.flags.contains(ColumnFlags::NOT_NULL)
406 }
407
408 #[must_use]
410 pub const fn primary_key(&self) -> bool {
411 self.flags.contains(ColumnFlags::PRIMARY_KEY)
412 }
413
414 #[must_use]
416 pub const fn unique(&self) -> bool {
417 self.flags.contains(ColumnFlags::UNIQUE)
418 }
419
420 #[must_use]
422 pub const fn has_default(&self) -> bool {
423 self.flags.contains(ColumnFlags::HAS_DEFAULT)
424 }
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq)]
429pub struct ColumnSqlRef {
430 pub table: &'static str,
431 pub name: &'static str,
432}
433
434impl ColumnSqlRef {
435 #[inline]
437 #[must_use]
438 pub const fn from_column_ref(column: ColumnRef) -> Self {
439 Self {
440 table: column.table,
441 name: column.name,
442 }
443 }
444
445 #[inline]
447 #[must_use]
448 pub const fn from_column_ref_ref(column: &ColumnRef) -> Self {
449 Self {
450 table: column.table,
451 name: column.name,
452 }
453 }
454}
455
456impl From<&ColumnRef> for ColumnSqlRef {
457 #[inline]
458 fn from(value: &ColumnRef) -> Self {
459 Self::from_column_ref_ref(value)
460 }
461}
462
463impl From<ColumnRef> for ColumnSqlRef {
464 #[inline]
465 fn from(value: ColumnRef) -> Self {
466 Self::from_column_ref(value)
467 }
468}
469
470#[inline]
486pub fn write_quoted_ident(buf: &mut impl core::fmt::Write, name: &str) {
487 write_dialect_quoted_ident(Dialect::SQLite, buf, name);
488}
489
490#[inline]
496pub(crate) fn write_dialect_quoted_ident(
497 dialect: Dialect,
498 buf: &mut impl core::fmt::Write,
499 name: &str,
500) {
501 let delimiter = match dialect {
502 Dialect::MySQL => '`',
503 Dialect::SQLite | Dialect::PostgreSQL => '"',
504 };
505
506 let _ = buf.write_char(delimiter);
507 if name.contains(delimiter) {
508 for ch in name.chars() {
509 if ch == delimiter {
510 let _ = buf.write_char(delimiter);
511 let _ = buf.write_char(delimiter);
512 } else {
513 let _ = buf.write_char(ch);
514 }
515 }
516 } else {
517 let _ = buf.write_str(name);
518 }
519 let _ = buf.write_char(delimiter);
520}
521
522#[derive(Clone)]
529pub enum SQLChunk<'a, V: SQLParam> {
530 Token(Token),
532
533 Ident(Cow<'a, str>),
536
537 Raw(Cow<'a, str>),
540
541 Number(usize),
544
545 Param(Param<'a, V>),
548
549 Table(TableSqlRef),
552
553 Column(ColumnSqlRef),
555}
556
557impl<'a, V: SQLParam> SQLChunk<'a, V> {
558 #[inline]
562 #[must_use]
563 pub const fn token(t: Token) -> Self {
564 Self::Token(t)
565 }
566
567 #[inline]
569 #[must_use]
570 pub const fn ident_static(name: &'static str) -> Self {
571 Self::Ident(Cow::Borrowed(name))
572 }
573
574 #[inline]
576 #[must_use]
577 pub const fn raw_static(text: &'static str) -> Self {
578 Self::Raw(Cow::Borrowed(text))
579 }
580
581 #[inline]
583 #[must_use]
584 pub const fn table(table: TableRef) -> Self {
585 Self::Table(TableSqlRef::from_table_ref(table))
586 }
587
588 #[inline]
590 #[must_use]
591 pub const fn column(column: ColumnRef) -> Self {
592 Self::Column(ColumnSqlRef::from_column_ref(column))
593 }
594
595 #[inline]
597 pub const fn param_borrowed(value: &'a V, placeholder: Placeholder) -> Self {
598 Self::Param(Param {
599 value: Some(Cow::Borrowed(value)),
600 placeholder,
601 })
602 }
603
604 #[inline]
608 pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
609 Self::Ident(name.into())
610 }
611
612 #[inline]
614 pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
615 Self::Raw(text.into())
616 }
617
618 #[inline]
620 #[must_use]
621 pub const fn number(value: usize) -> Self {
622 Self::Number(value)
623 }
624
625 #[inline]
627 pub fn param(value: impl Into<Cow<'a, V>>, placeholder: Placeholder) -> Self {
628 Self::Param(Param {
629 value: Some(value.into()),
630 placeholder,
631 })
632 }
633
634 #[inline]
638 pub(crate) fn write(&self, buf: &mut impl core::fmt::Write) {
639 match self {
640 SQLChunk::Token(token) => {
641 let _ = buf.write_str(token.as_str());
642 }
643 SQLChunk::Ident(name) => {
644 write_dialect_quoted_ident(V::DIALECT, buf, name);
645 }
646 SQLChunk::Raw(text) => {
647 let _ = buf.write_str(text);
648 }
649 SQLChunk::Number(value) => {
650 let _ = write!(buf, "{value}");
651 }
652 SQLChunk::Param(Param { placeholder, .. }) => {
653 let _ = write!(buf, "{placeholder}");
654 }
655 SQLChunk::Table(t) => {
656 if let Some(schema) = t.schema {
657 write_dialect_quoted_ident(V::DIALECT, buf, schema);
658 let _ = buf.write_char('.');
659 }
660 write_dialect_quoted_ident(V::DIALECT, buf, t.name);
661 }
662 SQLChunk::Column(c) => {
663 write_dialect_quoted_ident(V::DIALECT, buf, c.table);
664 let _ = buf.write_char('.');
665 write_dialect_quoted_ident(V::DIALECT, buf, c.name);
666 }
667 }
668 }
669
670 #[inline]
673 pub(crate) const fn is_word_like(&self) -> bool {
674 match self {
675 SQLChunk::Token(t) => !t.is_punctuation() && !t.is_operator(),
676 SQLChunk::Ident(_)
677 | SQLChunk::Raw(_)
678 | SQLChunk::Number(_)
679 | SQLChunk::Param(_)
680 | SQLChunk::Table(_)
681 | SQLChunk::Column(_) => true,
682 }
683 }
684}
685
686impl<V: SQLParam + core::fmt::Debug> core::fmt::Debug for SQLChunk<'_, V> {
687 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
688 match self {
689 SQLChunk::Token(token) => f.debug_tuple("Token").field(token).finish(),
690 SQLChunk::Ident(name) => f.debug_tuple("Ident").field(name).finish(),
691 SQLChunk::Raw(text) => f.debug_tuple("Raw").field(text).finish(),
692 SQLChunk::Number(value) => f.debug_tuple("Number").field(value).finish(),
693 SQLChunk::Param(param) => f.debug_tuple("Param").field(param).finish(),
694 SQLChunk::Table(t) => f
695 .debug_tuple("Table")
696 .field(&t.schema)
697 .field(&t.name)
698 .finish(),
699 SQLChunk::Column(c) => f
700 .debug_tuple("Column")
701 .field(&format!("{}.{}", c.table, c.name))
702 .finish(),
703 }
704 }
705}
706
707impl<V: SQLParam> From<Token> for SQLChunk<'_, V> {
710 #[inline]
711 fn from(value: Token) -> Self {
712 Self::Token(value)
713 }
714}
715
716impl<V: SQLParam> From<TableRef> for SQLChunk<'_, V> {
717 #[inline]
718 fn from(value: TableRef) -> Self {
719 Self::Table(value.into())
720 }
721}
722
723impl<V: SQLParam> From<ColumnRef> for SQLChunk<'_, V> {
724 #[inline]
725 fn from(value: ColumnRef) -> Self {
726 Self::Column(value.into())
727 }
728}
729
730impl<'a, V: SQLParam> From<Param<'a, V>> for SQLChunk<'a, V> {
731 #[inline]
732 fn from(value: Param<'a, V>) -> Self {
733 Self::Param(value)
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::dialect::{Dialect, MySQLDialect, SQLiteDialect};
741 use core::mem::size_of;
742
743 #[allow(dead_code)]
744 #[derive(Clone, Debug)]
745 struct TestParam([usize; 4]);
746
747 impl SQLParam for TestParam {
748 const DIALECT: Dialect = Dialect::SQLite;
749 type DialectMarker = SQLiteDialect;
750 }
751
752 #[derive(Clone, Debug)]
753 struct MySQLTestParam;
754
755 impl SQLParam for MySQLTestParam {
756 const DIALECT: Dialect = Dialect::MySQL;
757 type DialectMarker = MySQLDialect;
758 }
759
760 #[test]
761 fn sql_chunk_stays_slim() {
762 assert!(size_of::<SQLChunk<'static, TestParam>>() <= 64);
764 }
765
766 #[test]
767 fn quoted_ident_uses_the_dialect_delimiter() {
768 let mut sqlite = String::new();
769 write_dialect_quoted_ident(Dialect::SQLite, &mut sqlite, "account\"owner");
770 assert_eq!(sqlite, "\"account\"\"owner\"");
771
772 let mut postgres = String::new();
773 write_dialect_quoted_ident(Dialect::PostgreSQL, &mut postgres, "account\"owner");
774 assert_eq!(postgres, "\"account\"\"owner\"");
775
776 let mut mysql = String::new();
777 write_dialect_quoted_ident(Dialect::MySQL, &mut mysql, "account`owner");
778 assert_eq!(mysql, "`account``owner`");
779 }
780
781 #[test]
782 fn quoted_ident_keeps_injection_text_inside_the_identifier() {
783 let mut mysql = String::new();
784 write_dialect_quoted_ident(Dialect::MySQL, &mut mysql, "users`; DROP TABLE audit; --");
785 assert_eq!(mysql, "`users``; DROP TABLE audit; --`");
786 }
787
788 #[test]
789 fn table_chunk_preserves_structured_mysql_database_qualification() {
790 let table = TableRef {
791 schema: Some("tenant`db"),
792 ..TableRef::sql("user`accounts", &["id"])
793 };
794 let chunk = SQLChunk::<MySQLTestParam>::table(table);
795 let mut sql = String::new();
796
797 chunk.write(&mut sql);
798
799 assert_eq!(sql, "`tenant``db`.`user``accounts`");
800 }
801}