Skip to main content

fraiseql_core/runtime/
jsonb_strategy.rs

1//! JSONB field handling strategy selection
2//!
3//! Determines whether to project fields at database level or stream full JSONB
4
5use serde::Deserialize;
6
7/// Strategy for JSONB field handling
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9#[non_exhaustive]
10pub enum JsonbStrategy {
11    /// Extract only requested fields using `jsonb_build_object/JSON_OBJECT`
12    #[default]
13    Project,
14    /// Stream full JSONB column, filter in application
15    Stream,
16}
17
18impl std::str::FromStr for JsonbStrategy {
19    type Err = String;
20
21    fn from_str(s: &str) -> Result<Self, Self::Err> {
22        match s.to_lowercase().as_str() {
23            "project" => Ok(JsonbStrategy::Project),
24            "stream" => Ok(JsonbStrategy::Stream),
25            other => {
26                Err(format!("Invalid JSONB strategy '{}', must be 'project' or 'stream'", other))
27            },
28        }
29    }
30}
31
32impl<'de> Deserialize<'de> for JsonbStrategy {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        let s = String::deserialize(deserializer)?;
38        std::str::FromStr::from_str(&s).map_err(serde::de::Error::custom)
39    }
40}
41
42/// Configuration for JSONB optimization strategy
43#[derive(Debug, Clone)]
44pub struct JsonbOptimizationOptions {
45    /// Default strategy to use
46    pub default_strategy: JsonbStrategy,
47
48    /// Auto-switch threshold: if requesting >= this % of fields, use stream
49    pub auto_threshold_percent: u32,
50}
51
52impl Default for JsonbOptimizationOptions {
53    fn default() -> Self {
54        Self {
55            default_strategy:       JsonbStrategy::Project,
56            auto_threshold_percent: 80,
57        }
58    }
59}
60
61impl JsonbOptimizationOptions {
62    /// Choose strategy based on field count and configuration
63    pub fn choose_strategy(&self, requested_fields: usize, total_fields: usize) -> JsonbStrategy {
64        if total_fields == 0 {
65            return self.default_strategy;
66        }
67
68        #[allow(clippy::cast_precision_loss)]
69        // Reason: field counts are small integers; f64 precision loss on usize is not material
70        let percent = (requested_fields as f64 / total_fields as f64) * 100.0;
71
72        if percent >= f64::from(self.auto_threshold_percent) {
73            JsonbStrategy::Stream
74        } else {
75            self.default_strategy
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
83
84    use super::*;
85
86    // ========================================================================
87    // Strategy Parsing Tests
88    // ========================================================================
89
90    #[test]
91    fn test_jsonb_strategy_from_str_project() {
92        let strategy: JsonbStrategy = "project".parse().unwrap();
93        assert_eq!(strategy, JsonbStrategy::Project);
94    }
95
96    #[test]
97    fn test_jsonb_strategy_from_str_stream() {
98        let strategy: JsonbStrategy = "stream".parse().unwrap();
99        assert_eq!(strategy, JsonbStrategy::Stream);
100    }
101
102    #[test]
103    fn test_jsonb_strategy_from_str_case_insensitive() {
104        assert_eq!("PROJECT".parse::<JsonbStrategy>().unwrap(), JsonbStrategy::Project);
105        assert_eq!("Stream".parse::<JsonbStrategy>().unwrap(), JsonbStrategy::Stream);
106        assert_eq!("pRoJeCt".parse::<JsonbStrategy>().unwrap(), JsonbStrategy::Project);
107    }
108
109    #[test]
110    fn test_jsonb_strategy_from_str_invalid() {
111        let result = "invalid".parse::<JsonbStrategy>();
112        let err = result.expect_err("expected Err for invalid JSONB strategy string");
113        assert!(err.contains("Invalid JSONB strategy"), "unexpected error message: {err}");
114    }
115
116    #[test]
117    fn test_jsonb_strategy_deserialize() {
118        let json = r#""project""#;
119        let strategy: JsonbStrategy = serde_json::from_str(json).unwrap();
120        assert_eq!(strategy, JsonbStrategy::Project);
121    }
122
123    #[test]
124    fn test_jsonb_strategy_deserialize_stream() {
125        let json = r#""stream""#;
126        let strategy: JsonbStrategy = serde_json::from_str(json).unwrap();
127        assert_eq!(strategy, JsonbStrategy::Stream);
128    }
129
130    // ========================================================================
131    // Strategy Selection Tests
132    // ========================================================================
133
134    #[test]
135    fn test_choose_strategy_below_threshold() {
136        let opts = JsonbOptimizationOptions {
137            default_strategy:       JsonbStrategy::Project,
138            auto_threshold_percent: 80,
139        };
140
141        let strategy = opts.choose_strategy(5, 10);
142        assert_eq!(strategy, JsonbStrategy::Project);
143    }
144
145    #[test]
146    fn test_choose_strategy_at_threshold() {
147        let opts = JsonbOptimizationOptions {
148            default_strategy:       JsonbStrategy::Project,
149            auto_threshold_percent: 80,
150        };
151
152        let strategy = opts.choose_strategy(8, 10);
153        assert_eq!(strategy, JsonbStrategy::Stream);
154    }
155
156    #[test]
157    fn test_choose_strategy_above_threshold() {
158        let opts = JsonbOptimizationOptions {
159            default_strategy:       JsonbStrategy::Project,
160            auto_threshold_percent: 80,
161        };
162
163        let strategy = opts.choose_strategy(9, 10);
164        assert_eq!(strategy, JsonbStrategy::Stream);
165    }
166
167    #[test]
168    fn test_choose_strategy_respects_default() {
169        let opts = JsonbOptimizationOptions {
170            default_strategy:       JsonbStrategy::Stream,
171            auto_threshold_percent: 80,
172        };
173
174        let strategy = opts.choose_strategy(2, 10);
175        assert_eq!(strategy, JsonbStrategy::Stream);
176    }
177
178    #[test]
179    fn test_choose_strategy_zero_total() {
180        let opts = JsonbOptimizationOptions::default();
181        let strategy = opts.choose_strategy(0, 0);
182        assert_eq!(strategy, JsonbStrategy::Project);
183    }
184}