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