Skip to main content

zeph_db/
dialect.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// SQL fragments that differ between database backends.
5///
6/// Implemented by zero-sized marker types ([`Sqlite`], [`Postgres`]).
7/// All associated constants are `&'static str` for zero-cost usage.
8pub trait Dialect: Send + Sync + 'static {
9    /// Auto-increment primary key DDL fragment.
10    ///
11    /// `SQLite`: `INTEGER PRIMARY KEY AUTOINCREMENT`
12    /// `PostgreSQL`: `BIGSERIAL PRIMARY KEY`
13    const AUTO_PK: &'static str;
14
15    /// `INSERT OR IGNORE` prefix for this backend.
16    ///
17    /// `SQLite`: `INSERT OR IGNORE`
18    /// `PostgreSQL`: `INSERT` (pair with `CONFLICT_NOTHING` suffix)
19    const INSERT_IGNORE: &'static str;
20
21    /// Suffix for conflict-do-nothing semantics.
22    ///
23    /// `SQLite`: empty string (handled by `INSERT OR IGNORE` prefix)
24    /// `PostgreSQL`: `ON CONFLICT DO NOTHING`
25    const CONFLICT_NOTHING: &'static str;
26
27    /// Case-insensitive collation suffix for `ORDER BY` / `WHERE` clauses.
28    ///
29    /// `SQLite`: `COLLATE NOCASE`
30    /// `PostgreSQL`: empty string (use `ILIKE` or `LOWER()` instead)
31    const COLLATE_NOCASE: &'static str;
32
33    /// Current epoch seconds expression.
34    ///
35    /// `SQLite`: `unixepoch('now')`
36    /// `PostgreSQL`: `EXTRACT(EPOCH FROM NOW())::BIGINT`
37    const EPOCH_NOW: &'static str;
38
39    /// Case-insensitive comparison expression for a column.
40    ///
41    /// `SQLite`: `{col} COLLATE NOCASE`
42    /// `PostgreSQL`: `LOWER({col})`
43    fn ilike(col: &str) -> String;
44
45    /// Epoch seconds expression for a timestamp column.
46    ///
47    /// Wraps the column in the backend-specific function that converts a stored
48    /// timestamp to a Unix epoch integer, coalescing `NULL` to `0`.
49    ///
50    /// `SQLite`: `COALESCE(CAST(strftime('%s', {col}) AS INTEGER), 0)`
51    /// `PostgreSQL`: `COALESCE(CAST(EXTRACT(EPOCH FROM {col}) AS BIGINT), 0)`
52    fn epoch_from_col(col: &str) -> String;
53}
54
55/// `SQLite` dialect marker type.
56pub struct Sqlite;
57
58impl Dialect for Sqlite {
59    const AUTO_PK: &'static str = "INTEGER PRIMARY KEY AUTOINCREMENT";
60    const INSERT_IGNORE: &'static str = "INSERT OR IGNORE";
61    const CONFLICT_NOTHING: &'static str = "";
62    const COLLATE_NOCASE: &'static str = "COLLATE NOCASE";
63    const EPOCH_NOW: &'static str = "unixepoch('now')";
64
65    fn ilike(col: &str) -> String {
66        format!("{col} COLLATE NOCASE")
67    }
68
69    fn epoch_from_col(col: &str) -> String {
70        format!("COALESCE(CAST(strftime('%s', {col}) AS INTEGER), 0)")
71    }
72}
73
74/// `PostgreSQL` dialect marker type.
75pub struct Postgres;
76
77impl Dialect for Postgres {
78    const AUTO_PK: &'static str = "BIGSERIAL PRIMARY KEY";
79    const INSERT_IGNORE: &'static str = "INSERT";
80    const CONFLICT_NOTHING: &'static str = "ON CONFLICT DO NOTHING";
81    const COLLATE_NOCASE: &'static str = "";
82    const EPOCH_NOW: &'static str = "EXTRACT(EPOCH FROM NOW())::BIGINT";
83
84    fn ilike(col: &str) -> String {
85        format!("LOWER({col})")
86    }
87
88    fn epoch_from_col(col: &str) -> String {
89        format!("COALESCE(CAST(EXTRACT(EPOCH FROM {col}) AS BIGINT), 0)")
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[cfg(feature = "sqlite")]
98    mod sqlite {
99        use super::*;
100
101        #[test]
102        fn auto_pk() {
103            assert_eq!(Sqlite::AUTO_PK, "INTEGER PRIMARY KEY AUTOINCREMENT");
104        }
105
106        #[test]
107        fn insert_ignore() {
108            assert_eq!(Sqlite::INSERT_IGNORE, "INSERT OR IGNORE");
109            assert_eq!(Sqlite::CONFLICT_NOTHING, "");
110        }
111
112        #[test]
113        fn epoch_now() {
114            assert_eq!(Sqlite::EPOCH_NOW, "unixepoch('now')");
115        }
116
117        #[test]
118        fn epoch_from_col() {
119            assert_eq!(
120                Sqlite::epoch_from_col("created_at"),
121                "COALESCE(CAST(strftime('%s', created_at) AS INTEGER), 0)"
122            );
123        }
124
125        #[test]
126        fn ilike() {
127            assert_eq!(Sqlite::ilike("name"), "name COLLATE NOCASE");
128        }
129    }
130
131    #[cfg(feature = "postgres")]
132    mod postgres {
133        use super::*;
134
135        #[test]
136        fn auto_pk() {
137            assert_eq!(Postgres::AUTO_PK, "BIGSERIAL PRIMARY KEY");
138        }
139
140        #[test]
141        fn insert_ignore() {
142            assert_eq!(Postgres::INSERT_IGNORE, "INSERT");
143            assert_eq!(Postgres::CONFLICT_NOTHING, "ON CONFLICT DO NOTHING");
144        }
145
146        #[test]
147        fn epoch_now() {
148            assert_eq!(Postgres::EPOCH_NOW, "EXTRACT(EPOCH FROM NOW())::BIGINT");
149        }
150
151        #[test]
152        fn epoch_from_col() {
153            assert_eq!(
154                Postgres::epoch_from_col("created_at"),
155                "COALESCE(CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT), 0)"
156            );
157        }
158
159        #[test]
160        fn ilike() {
161            assert_eq!(Postgres::ilike("name"), "LOWER(name)");
162        }
163    }
164}