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;