Skip to main content

drizzle_cli/commands/
init.rs

1//! `drizzle init`: writes a starter `drizzle.config.toml`.
2
3use crate::config::CONFIG_FILE;
4use crate::error::CliError;
5use crate::output;
6use std::path::Path;
7
8/// JSON schema URL for TOML validation
9const SCHEMA_URL: &str =
10    "https://raw.githubusercontent.com/themixednuts/drizzle-rs/main/cli/schema.json";
11
12/// Default `schema` value of a new config.
13pub const DEFAULT_SCHEMA: &str = "src/schema.rs";
14
15/// Default `out` value of a new config.
16pub const DEFAULT_OUT: &str = "./drizzle";
17
18/// Runs `drizzle init`: writes [`CONFIG_FILE`] in the current directory.
19///
20/// # Errors
21///
22/// Returns [`CliError`] if the file already exists, the dialect or driver is
23/// unknown, or the file cannot be written.
24pub fn run(dialect: &str, driver: Option<&str>) -> Result<(), CliError> {
25    let config_path = Path::new(CONFIG_FILE);
26
27    if config_path.exists() {
28        return Err(CliError::Other(format!(
29            "{CONFIG_FILE} already exists. Delete it first to reinitialize."
30        )));
31    }
32
33    let config_content = config_contents(dialect, driver, DEFAULT_SCHEMA, DEFAULT_OUT)?;
34
35    std::fs::write(config_path, config_content).map_err(|e| CliError::IoError(e.to_string()))?;
36
37    println!("{}", output::success(&format!("Created {CONFIG_FILE}")));
38    println!();
39    println!("Next steps:");
40    println!("  1. Edit {CONFIG_FILE} with your database credentials");
41    println!(
42        "  2. Create your schema file at {}",
43        output::heading(DEFAULT_SCHEMA)
44    );
45    println!(
46        "  3. Run {} to generate your first migration",
47        output::heading("drizzle generate")
48    );
49
50    Ok(())
51}
52
53/// Returns the text of a starter config for `dialect` (and optionally
54/// `driver`), with `schema` and `out` set to the given paths.
55///
56/// Accepts the dialects `sqlite`, `turso`, `postgresql` (or `postgres`) and
57/// `mysql`, and only the Rust drivers of that dialect.
58///
59/// # Errors
60///
61/// Returns [`CliError::Other`] for an unknown dialect or a driver that does
62/// not belong to it.
63pub fn config_contents(
64    dialect: &str,
65    driver: Option<&str>,
66    schema: &str,
67    out: &str,
68) -> Result<String, CliError> {
69    let dialect = dialect.to_lowercase();
70    let driver = driver.map(str::to_lowercase);
71    let schema = toml_string(schema);
72    let out = toml_string(out);
73
74    // Rust-only: keep init output aligned with what `cli/src/config.rs` can actually parse.
75    match dialect.as_str() {
76        "sqlite" => {
77            if let Some(ref d) = driver
78                && d != "rusqlite"
79            {
80                return Err(CliError::Other(format!(
81                    "Invalid driver for sqlite: {d}. Supported: rusqlite"
82                )));
83            }
84            Ok(format!(
85                r#"#:schema {SCHEMA_URL}
86
87# Drizzle Configuration (drizzle-rs)
88#
89# This file is parsed by `drizzle-cli` and should stay aligned with its config schema:
90# - dialect: sqlite | turso | postgresql | mysql
91# - drivers: Rust drivers only (optional)
92
93dialect = "sqlite"
94# driver = "rusqlite"
95schema = {schema}
96out = {out}
97# breakpoints = true
98
99[dbCredentials]
100url = "./dev.db"
101"#
102            ))
103        }
104        "turso" => {
105            if let Some(ref d) = driver
106                && d != "libsql"
107                && d != "turso"
108            {
109                return Err(CliError::Other(format!(
110                    "Invalid driver for turso: {d}. Supported: libsql, turso"
111                )));
112            }
113            Ok(format!(
114                r#"#:schema {SCHEMA_URL}
115
116# Drizzle Configuration (drizzle-rs)
117
118dialect = "turso"
119# driver = "libsql"   # local libsql (embedded)
120# driver = "turso"    # remote Turso
121schema = {schema}
122out = {out}
123# breakpoints = true
124
125[dbCredentials]
126url = "libsql://your-db.turso.io"
127authToken = "your-auth-token"
128"#
129            ))
130        }
131        "postgresql" | "postgres" => {
132            if let Some(ref d) = driver
133                && d != "postgres-sync"
134                && d != "tokio-postgres"
135            {
136                return Err(CliError::Other(format!(
137                    "Invalid driver for postgresql: {d}. Supported: postgres-sync, tokio-postgres"
138                )));
139            }
140            Ok(format!(
141                r#"#:schema {SCHEMA_URL}
142
143# Drizzle Configuration (drizzle-rs)
144
145dialect = "postgresql"
146# driver = "postgres-sync"
147# driver = "tokio-postgres"
148schema = {schema}
149out = {out}
150# breakpoints = true
151
152[dbCredentials]
153url = "postgres://user:password@localhost:5432/mydb"
154
155# Or use individual connection fields:
156# [dbCredentials]
157# host = "localhost"
158# port = 5432
159# user = "postgres"
160# password = "password"
161# database = "mydb"
162# ssl = true
163"#
164            ))
165        }
166        "mysql" => {
167            if let Some(ref driver) = driver
168                && driver != "mysql-sync"
169                && driver != "mysql-async"
170            {
171                return Err(CliError::Other(format!(
172                    "Invalid driver for mysql: {driver}. Supported: mysql-sync, mysql-async"
173                )));
174            }
175            let driver_config = driver.as_deref().map_or_else(
176                || "# driver = \"mysql-sync\"\n# driver = \"mysql-async\"".to_string(),
177                |driver| format!("driver = \"{driver}\""),
178            );
179            Ok(format!(
180                r#"#:schema {SCHEMA_URL}
181
182# Drizzle Configuration (drizzle-rs)
183
184dialect = "mysql"
185{driver_config}
186schema = {schema}
187out = {out}
188# breakpoints = true
189
190[dbCredentials]
191url = "mysql://user:password@localhost:3306/mydb"
192
193# Or use individual connection fields:
194# [dbCredentials]
195# host = "localhost"
196# port = 3306
197# user = "root"
198# password = "password"
199# database = "mydb"
200# ssl = "required"
201"#
202            ))
203        }
204        _ => Err(CliError::Other(format!(
205            "Unknown dialect: {dialect}. Supported: sqlite, turso, postgresql, mysql"
206        ))),
207    }
208}
209
210/// Quotes `value` as a TOML string (backslashes in Windows paths included).
211fn toml_string(value: &str) -> String {
212    toml::Value::String(value.to_string()).to_string()
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn mysql_init_template_uses_mysql_port_and_drivers() {
221        let config = config_contents("mysql", Some("mysql-sync"), DEFAULT_SCHEMA, DEFAULT_OUT)
222            .expect("mysql config");
223        assert!(config.contains("dialect = \"mysql\""), "{config}");
224        assert!(config.contains("mysql://user:password@localhost:3306/mydb"));
225        assert!(config.contains("driver = \"mysql-sync\""), "{config}");
226        assert!(!config.contains("ssl = \"preferred\""), "{config}");
227
228        let async_config =
229            config_contents("mysql", Some("mysql-async"), DEFAULT_SCHEMA, DEFAULT_OUT)
230                .expect("async mysql config");
231        assert!(
232            async_config.contains("driver = \"mysql-async\""),
233            "{async_config}"
234        );
235        assert!(
236            config_contents("mysql", Some("postgres-sync"), DEFAULT_SCHEMA, DEFAULT_OUT).is_err()
237        );
238    }
239
240    #[test]
241    fn template_paths_are_quoted_and_parse_back() {
242        for dialect in ["sqlite", "turso", "postgresql", "mysql"] {
243            let text = config_contents(dialect, None, "src/db/schema.rs", "C:\\app\\drizzle")
244                .expect("config");
245            let value: toml::Table = toml::from_str(&text).expect("valid TOML");
246            assert_eq!(value["schema"].as_str(), Some("src/db/schema.rs"));
247            assert_eq!(value["out"].as_str(), Some("C:\\app\\drizzle"));
248        }
249    }
250}