Skip to main content

dactyl_db/query/
dialect.rs

1//! Dialect detection for the lexical analyzer.
2//!
3//! This is the *first-pass* scope: the analyzer recognizes the explicit list of
4//! constructs the project ships with and treats anything else as portable SQL.
5//! A full SQL parser is intentionally out of scope — see the issue tracker for
6//! the follow-up that adds the rewriter.
7
8/// SQL dialect an adapter speaks natively.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Dialect {
11    /// Local file-backed SQLite.
12    Sqlite,
13    /// Remote Postgres via Neon HTTP (Propodus).
14    Postgres,
15}
16
17/// A single dialect-specific construct the analyzer found in a query.
18///
19/// Anything not enumerated here is treated as portable SQL and never produces a
20/// `Construct`. This keeps the dialect-mismatch check tight.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Construct {
23    // SQLite-only
24    JsonEach,
25    JsonTree,
26    WithoutRowId,
27    Strict,
28    // Postgres-only
29    JsonArrowText,
30    JsonArrow,
31    JsonContains,
32    JsonContained,
33    JsonExists,
34    Jsonb,
35    Returning,
36    Ilike,
37    GenRandomUuid,
38    NowFn,
39}
40
41impl Construct {
42    /// Return the lexeme that triggered detection.
43    pub fn lexeme(self) -> &'static str {
44        match self {
45            Construct::JsonEach => "json_each",
46            Construct::JsonTree => "json_tree",
47            Construct::WithoutRowId => "without rowid",
48            Construct::Strict => "strict",
49            Construct::JsonArrowText => "->>",
50            Construct::JsonArrow => "->",
51            Construct::JsonContains => "@>",
52            Construct::JsonContained => "<@",
53            Construct::JsonExists => "?",
54            Construct::Jsonb => "jsonb",
55            Construct::Returning => "returning",
56            Construct::Ilike => "ilike",
57            Construct::GenRandomUuid => "gen_random_uuid",
58            Construct::NowFn => "now",
59        }
60    }
61
62    /// The dialect this construct belongs to.
63    pub fn dialect(self) -> Dialect {
64        match self {
65            Construct::JsonEach
66            | Construct::JsonTree
67            | Construct::WithoutRowId
68            | Construct::Strict => Dialect::Sqlite,
69            _ => Dialect::Postgres,
70        }
71    }
72
73    /// Whether the dialect natively supports this construct.
74    pub fn supported_by(self, dialect: Dialect) -> bool {
75        self.dialect() == dialect
76    }
77}
78
79/// Decide whether the construct list is portable enough for the active dialect.
80///
81/// Returns the first unsupported construct, or `None` if every construct is
82/// supported (or the list is empty).
83pub fn first_unsupported(constructs: &[Construct], dialect: Dialect) -> Option<Construct> {
84    constructs
85        .iter()
86        .copied()
87        .find(|c| !c.supported_by(dialect))
88}