Skip to main content

dactyl_db/query/
dialect.rs

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