Skip to main content

drizzle_types/
migration.rs

1use crate::alloc_prelude::*;
2
3/// Identifier casing strategy for inferred names.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
7pub enum Casing {
8    /// `camelCase` (e.g. `userId`, `createdAt`).
9    #[default]
10    #[cfg_attr(feature = "serde", serde(rename = "camelCase"))]
11    CamelCase,
12    /// `snake_case` (e.g. `user_id`, `created_at`).
13    #[cfg_attr(feature = "serde", serde(rename = "snake_case"))]
14    SnakeCase,
15}
16
17impl Casing {
18    /// Returns the config spelling: `"camelCase"` or `"snake_case"`.
19    #[must_use]
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::CamelCase => "camelCase",
23            Self::SnakeCase => "snake_case",
24        }
25    }
26}
27
28impl core::fmt::Display for Casing {
29    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30        f.write_str(self.as_str())
31    }
32}
33
34#[cfg(any(feature = "std", feature = "alloc"))]
35impl core::str::FromStr for Casing {
36    type Err = crate::alloc_prelude::String;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        match s {
40            "camelCase" | "camel" => Ok(Self::CamelCase),
41            "snake_case" | "snake" => Ok(Self::SnakeCase),
42            _ => Err(format!(
43                "invalid casing '{s}', expected 'camelCase' or 'snake_case'"
44            )),
45        }
46    }
47}
48
49/// Where applied migrations are recorded: the tracking table and its schema.
50///
51/// Shared by the CLI and the runtime migrator. Each dialect has a default
52/// constant; adjust it with the builder methods.
53///
54/// # Examples
55///
56/// ```
57/// use drizzle_types::MigrationTracking;
58///
59/// let tracking = MigrationTracking::POSTGRES.table("schema_history");
60/// assert_eq!(tracking.table, "schema_history");
61/// assert_eq!(tracking.schema.as_deref(), Some("drizzle"));
62/// ```
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct MigrationTracking {
65    /// Migrations tracking table name.
66    pub table: Cow<'static, str>,
67    /// Schema of the tracking table (used by `PostgreSQL`; `None` elsewhere).
68    pub schema: Option<Cow<'static, str>>,
69}
70
71impl MigrationTracking {
72    /// `SQLite` default: table `__drizzle_migrations`, no schema.
73    pub const SQLITE: Self = Self {
74        table: Cow::Borrowed("__drizzle_migrations"),
75        schema: None,
76    };
77
78    /// `PostgreSQL` default: table `__drizzle_migrations` in schema `drizzle`.
79    pub const POSTGRES: Self = Self {
80        table: Cow::Borrowed("__drizzle_migrations"),
81        schema: Some(Cow::Borrowed("drizzle")),
82    };
83
84    /// `MySQL` default: table `__drizzle_migrations` in the connection's database.
85    pub const MYSQL: Self = Self {
86        table: Cow::Borrowed("__drizzle_migrations"),
87        schema: None,
88    };
89
90    /// Creates tracking metadata from a table name and an optional schema.
91    pub fn new(
92        table: impl Into<Cow<'static, str>>,
93        schema: Option<impl Into<Cow<'static, str>>>,
94    ) -> Self {
95        Self {
96            table: table.into(),
97            schema: schema.map(Into::into),
98        }
99    }
100
101    /// Replaces the table name, keeping the schema.
102    #[must_use]
103    pub fn table(mut self, table: impl Into<Cow<'static, str>>) -> Self {
104        self.table = table.into();
105        self
106    }
107
108    /// Sets the schema, keeping the table name.
109    #[must_use]
110    pub fn schema(mut self, schema: impl Into<Cow<'static, str>>) -> Self {
111        self.schema = Some(schema.into());
112        self
113    }
114
115    /// Removes the schema, keeping the table name.
116    #[must_use]
117    pub fn without_schema(mut self) -> Self {
118        self.schema = None;
119        self
120    }
121}
122impl Default for MigrationTracking {
123    fn default() -> Self {
124        Self::SQLITE
125    }
126}
127
128/// A config value written inline or read from an environment variable.
129///
130/// With the `serde` feature it deserializes from `"literal"` or
131/// `{ env = "VAR_NAME" }`, the same shape `drizzle-kit` and the CLI accept
132/// for `dbCredentials.url`.
133///
134/// # Examples
135///
136/// ```
137/// use drizzle_types::ConfigValue;
138///
139/// let url = ConfigValue::Inline("postgres://localhost/app".into());
140/// assert_eq!(url.resolve().unwrap(), "postgres://localhost/app");
141/// ```
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum ConfigValue {
144    /// Value written inline in the config file.
145    Inline(String),
146    /// Name of the environment variable to resolve.
147    Env(String),
148}
149
150#[cfg(feature = "std")]
151impl ConfigValue {
152    /// Returns the value, reading the environment variable for [`ConfigValue::Env`].
153    ///
154    /// # Errors
155    ///
156    /// Returns [`ConfigValueError::NotPresent`] if this is a [`ConfigValue::Env`] pointing
157    /// to a variable that is not set, or [`ConfigValueError::NotUnicode`] if the
158    /// variable is set but contains invalid UTF-8.
159    pub fn resolve(&self) -> Result<String, ConfigValueError> {
160        match self {
161            Self::Inline(v) => Ok(v.clone()),
162            Self::Env(var) => match std::env::var(var) {
163                Ok(v) => Ok(v),
164                Err(std::env::VarError::NotPresent) => {
165                    Err(ConfigValueError::NotPresent(var.clone()))
166                }
167                Err(std::env::VarError::NotUnicode(_)) => {
168                    Err(ConfigValueError::NotUnicode(var.clone()))
169                }
170            },
171        }
172    }
173
174    /// Returns the value, or `None` when a [`ConfigValue::Env`] variable is unset.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`ConfigValueError::NotUnicode`] if the env var is set but contains
179    /// invalid UTF-8. Missing env vars resolve to `Ok(None)`.
180    pub fn resolve_optional(&self) -> Result<Option<String>, ConfigValueError> {
181        match self {
182            Self::Inline(v) => Ok(Some(v.clone())),
183            Self::Env(var) => match std::env::var(var) {
184                Ok(v) => Ok(Some(v)),
185                Err(std::env::VarError::NotPresent) => Ok(None),
186                Err(std::env::VarError::NotUnicode(_)) => {
187                    Err(ConfigValueError::NotUnicode(var.clone()))
188                }
189            },
190        }
191    }
192}
193
194/// Failure resolving a [`ConfigValue::Env`] reference.
195#[cfg(feature = "std")]
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum ConfigValueError {
198    /// The named environment variable is not set in the process.
199    NotPresent(String),
200    /// The named environment variable is set but contains non-UTF-8 bytes.
201    NotUnicode(String),
202}
203
204#[cfg(feature = "std")]
205impl core::fmt::Display for ConfigValueError {
206    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
207        match self {
208            Self::NotPresent(var) => write!(f, "env var `{var}` not set"),
209            Self::NotUnicode(var) => write!(f, "env var `{var}` contains invalid unicode"),
210        }
211    }
212}
213
214#[cfg(feature = "std")]
215impl std::error::Error for ConfigValueError {}
216
217#[cfg(feature = "serde")]
218impl<'de> serde::Deserialize<'de> for ConfigValue {
219    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
220    where
221        D: serde::Deserializer<'de>,
222    {
223        use serde::de::{self, MapAccess, Visitor};
224
225        struct ConfigValueVisitor;
226
227        impl<'de> Visitor<'de> for ConfigValueVisitor {
228            type Value = ConfigValue;
229
230            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
231                formatter.write_str("a string or { env = \"VAR_NAME\" }")
232            }
233
234            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
235            where
236                E: de::Error,
237            {
238                Ok(ConfigValue::Inline(value.to_string()))
239            }
240
241            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
242            where
243                M: MapAccess<'de>,
244            {
245                let mut env_var: Option<String> = None;
246
247                while let Some(key) = map.next_key::<String>()? {
248                    if key == "env" {
249                        env_var = Some(map.next_value()?);
250                    } else {
251                        return Err(de::Error::unknown_field(&key, &["env"]));
252                    }
253                }
254
255                env_var
256                    .map(ConfigValue::Env)
257                    .ok_or_else(|| de::Error::missing_field("env"))
258            }
259        }
260
261        deserializer.deserialize_any(ConfigValueVisitor)
262    }
263}
264
265#[cfg(feature = "schemars")]
266impl schemars::JsonSchema for ConfigValue {
267    fn schema_name() -> Cow<'static, str> {
268        "ConfigValue".into()
269    }
270
271    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
272        use schemars::json_schema;
273
274        // ConfigValue accepts either a plain string or { env: "VAR_NAME" }
275        json_schema!({
276            "oneOf": [
277                generator.subschema_for::<String>(),
278                {
279                    "type": "object",
280                    "properties": {
281                        "env": { "type": "string" }
282                    },
283                    "required": ["env"],
284                    "additionalProperties": false
285                }
286            ]
287        })
288    }
289}