Skip to main content

fraiseql_db/dialect/
capability.rs

1//! JSON-extraction expressions keyed on [`DatabaseType`].
2//!
3//! Historical note: this module used to hold a `DialectCapabilityGuard` and a
4//! per-dialect feature matrix. Three audit passes found the guard was never
5//! called from any production path, and the G2 decision (#374) removed the
6//! non-PostgreSQL dialects it existed to arbitrate — so it was deleted rather
7//! than wired. What remains is the single source of truth for "how does a
8//! `DatabaseType` extract a value from the `data` JSONB column".
9
10use super::SqlDialect;
11use crate::types::{DatabaseType, sql_hints::ScalarFieldType};
12
13impl DatabaseType {
14    /// Return a SQL expression that extracts a text value from the `data` JSONB column.
15    ///
16    /// The `key` must already be validated via `OrderByClause::validate_field_name`
17    /// and converted to snake_case storage form via `OrderByClause::storage_key`.
18    ///
19    /// # Examples
20    ///
21    /// ```
22    /// use fraiseql_db::DatabaseType;
23    ///
24    /// assert_eq!(DatabaseType::PostgreSQL.json_field_expr("created_at"), "data->>'created_at'");
25    /// ```
26    #[must_use]
27    pub fn json_field_expr(self, key: &str) -> String {
28        match self {
29            Self::PostgreSQL => format!("data->>'{key}'"),
30        }
31    }
32
33    /// Return the [`SqlDialect`] that renders SQL for this database.
34    ///
35    /// The two dialect abstractions — this enum, used by the ORDER BY renderer,
36    /// and the [`SqlDialect`] trait, used by the WHERE generator — resolve to
37    /// the same implementation here, so neither can carry its own copy of a
38    /// rendering rule.
39    #[must_use]
40    pub fn dialect(self) -> &'static dyn SqlDialect {
41        match self {
42            Self::PostgreSQL => &super::PostgresDialect,
43        }
44    }
45
46    /// Return a SQL expression that extracts and casts a value from the `data` JSONB
47    /// column for ORDER BY sorting.
48    ///
49    /// When `field_type` is [`ScalarFieldType::Text`] this is identical to
50    /// [`json_field_expr`](Self::json_field_expr). For numeric, date, and boolean
51    /// types the expression is wrapped in a dialect-specific cast so the database
52    /// sorts by the typed value instead of the raw text (`"9" > "10"` is wrong for
53    /// numbers).
54    ///
55    /// The cast itself comes from [`SqlDialect::cast_expr_as`] — the same call the
56    /// WHERE generator makes — so an ORDER BY and a filter on one field agree.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use fraiseql_db::{DatabaseType, ScalarFieldType};
62    ///
63    /// assert_eq!(
64    ///     DatabaseType::PostgreSQL.typed_json_field_expr("amount", ScalarFieldType::Numeric),
65    ///     "(data->>'amount')::numeric"
66    /// );
67    /// ```
68    #[must_use]
69    pub fn typed_json_field_expr(self, key: &str, field_type: ScalarFieldType) -> String {
70        let base = self.json_field_expr(key);
71        self.dialect().cast_expr_as(&base, field_type).into_owned()
72    }
73}
74
75#[cfg(test)]
76mod tests;