Skip to main content

fraiseql_db/
collation_config.rs

1//! Collation configuration for user-aware sorting.
2//!
3//! Maps user locales to database-specific collation strings.
4
5use serde::{Deserialize, Serialize};
6
7/// Collation configuration for user-aware sorting.
8///
9/// This configuration enables automatic collation support based on user locale,
10/// adapting to database capabilities.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(default)]
13pub struct CollationConfig {
14    /// Enable automatic user-aware collation.
15    pub enabled: bool,
16
17    /// Fallback locale for unauthenticated users.
18    pub fallback_locale: String,
19
20    /// Allowed locales (whitelist for security).
21    pub allowed_locales: Vec<String>,
22
23    /// Strategy when user locale is not in allowed list.
24    pub on_invalid_locale: InvalidLocaleStrategy,
25
26    /// Database-specific overrides (optional).
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub database_overrides: Option<DatabaseCollationOverrides>,
29}
30
31impl Default for CollationConfig {
32    fn default() -> Self {
33        Self {
34            enabled:            true,
35            fallback_locale:    "en-US".to_string(),
36            allowed_locales:    vec![
37                "en-US".into(),
38                "en-GB".into(),
39                "fr-FR".into(),
40                "de-DE".into(),
41                "es-ES".into(),
42                "ja-JP".into(),
43                "zh-CN".into(),
44                "pt-BR".into(),
45                "it-IT".into(),
46            ],
47            on_invalid_locale:  InvalidLocaleStrategy::Fallback,
48            database_overrides: None,
49        }
50    }
51}
52
53/// Strategy when user locale is not in allowed list.
54#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum InvalidLocaleStrategy {
58    /// Use fallback locale.
59    #[default]
60    Fallback,
61    /// Use database default (no COLLATE clause).
62    DatabaseDefault,
63    /// Return error.
64    Error,
65}
66
67/// Database-specific collation overrides.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct DatabaseCollationOverrides {
70    /// PostgreSQL-specific settings.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub postgres: Option<PostgresCollationConfig>,
73
74    /// MySQL-specific settings.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub mysql: Option<MySqlCollationConfig>,
77
78    /// SQLite-specific settings.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub sqlite: Option<SqliteCollationConfig>,
81
82    /// SQL Server-specific settings.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub sqlserver: Option<SqlServerCollationConfig>,
85}
86
87/// PostgreSQL-specific collation configuration.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PostgresCollationConfig {
90    /// Use ICU collations (recommended).
91    pub use_icu: bool,
92
93    /// Provider: "icu" or "libc".
94    pub provider: String,
95}
96
97impl Default for PostgresCollationConfig {
98    fn default() -> Self {
99        Self {
100            use_icu:  true,
101            provider: "icu".to_string(),
102        }
103    }
104}
105
106/// MySQL-specific collation configuration.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct MySqlCollationConfig {
109    /// Charset (e.g., "utf8mb4").
110    pub charset: String,
111
112    /// Collation suffix (e.g., "_unicode_ci" or "_0900_ai_ci").
113    pub suffix: String,
114}
115
116impl Default for MySqlCollationConfig {
117    fn default() -> Self {
118        Self {
119            charset: "utf8mb4".to_string(),
120            suffix:  "_unicode_ci".to_string(),
121        }
122    }
123}
124
125/// SQLite-specific collation configuration.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct SqliteCollationConfig {
128    /// Use COLLATE NOCASE for case-insensitive sorting.
129    pub use_nocase: bool,
130}
131
132impl Default for SqliteCollationConfig {
133    fn default() -> Self {
134        Self { use_nocase: true }
135    }
136}
137
138/// SQL Server-specific collation configuration.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct SqlServerCollationConfig {
141    /// Case-insensitive (CI) collations.
142    pub case_insensitive: bool,
143
144    /// Accent-insensitive (AI) collations.
145    pub accent_insensitive: bool,
146}
147
148impl Default for SqlServerCollationConfig {
149    fn default() -> Self {
150        Self {
151            case_insensitive:   true,
152            accent_insensitive: true,
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests;