diff --git a/book/docs/connect/README.md b/book/docs/connect/README.md index 068f4834..41768914 100644 --- a/book/docs/connect/README.md +++ b/book/docs/connect/README.md @@ -20,7 +20,35 @@ based config and specify connection details for those databases. [Please check h ### 2. CLI options -Run the following command for more details +You can provide database connection details via CLI options. This includes individual connection parameters or a complete database URL. + +#### Using individual connection parameters + +```bash +$ sqlx-ts \ + --db-type postgres \ + --db-host 127.0.0.1 \ + --db-port 5432 \ + --db-user postgres \ + --db-pass postgres \ + --db-name mydb +``` + +#### Using database URL + +Alternatively, you can use `--db-url` to specify the complete connection string: + +```bash +# PostgreSQL +$ sqlx-ts --db-type postgres --db-url postgres://user:pass@localhost:5432/mydb + +# MySQL +$ sqlx-ts --db-type mysql --db-url mysql://user:pass@localhost:3306/mydb +``` + +**Note:** When `--db-url` is provided, it takes precedence over individual connection parameters (`--db-host`, `--db-port`, `--db-user`, `--db-pass`, `--db-name`). + +Run the following command for more details: ```bash $ sqlx-ts --help diff --git a/book/docs/connect/config-file.md b/book/docs/connect/config-file.md index abc42628..a0280fda 100644 --- a/book/docs/connect/config-file.md +++ b/book/docs/connect/config-file.md @@ -46,6 +46,26 @@ Example `.sqlxrc.json` } ``` +Alternatively, you can use `DB_URL` to specify the connection string directly: + +```json +{ + "generate_types": { + "enabled": true + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_URL": "postgres://postgres:postgres@127.0.0.1:5432/mydb" + }, + "mysql_db": { + "DB_TYPE": "mysql", + "DB_URL": "mysql://root:password@127.0.0.1:3306/mydatabase" + } + } +} +``` + ## Configuration options ### connections (required) @@ -71,6 +91,7 @@ const postgresSQL = sql` ``` Supported fields of each connection include +- `DB_URL`: Database connection URL (e.g. `postgres://user:pass@host:port/dbname` or `mysql://user:pass@host:port/dbname`). If provided, this overrides individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`) - `DB_TYPE`: type of database connection (mysql | postgres) - `DB_USER`: database user name - `DB_PASS`: database password diff --git a/book/docs/connect/environment-variables.md b/book/docs/connect/environment-variables.md index c7186476..d57574ad 100644 --- a/book/docs/connect/environment-variables.md +++ b/book/docs/connect/environment-variables.md @@ -3,6 +3,7 @@ | Environment variables | Description | | --------------------- | ------------------------------------------------------------------------------------------ | +| DB_URL | Primary database connection URL (e.g. `postgres://user:pass@host:port/dbname` or `mysql://user:pass@host:port/dbname`). If provided, this overrides individual connection parameters | | DB_HOST | Primary DB host | | DB_PASS | Primary DB password | | DB_PORT | Primary DB port number | @@ -11,3 +12,38 @@ | DB_NAME | Primary DB name | | PG_SEARCH_PATH | PostgreSQL schema search path (default is "$user,public") [https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH](https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH) | +## Examples + +### Using individual connection parameters + +```bash +export DB_TYPE=postgres +export DB_HOST=127.0.0.1 +export DB_PORT=5432 +export DB_USER=postgres +export DB_PASS=postgres +export DB_NAME=mydb + +sqlx-ts +``` + +### Using database URL + +Alternatively, you can use `DB_URL` to specify the complete connection string: + +```bash +# PostgreSQL +export DB_TYPE=postgres +export DB_URL=postgres://postgres:postgres@localhost:5432/mydb + +sqlx-ts + +# MySQL +export DB_TYPE=mysql +export DB_URL=mysql://root:password@localhost:3306/mydatabase + +sqlx-ts +``` + +**Note:** When `DB_URL` is set, it takes precedence over individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`). + diff --git a/src/common/cli.rs b/src/common/cli.rs index 51a9dff0..1e750064 100644 --- a/src/common/cli.rs +++ b/src/common/cli.rs @@ -52,6 +52,10 @@ pub struct Cli { #[clap(long)] pub db_name: Option, + /// Custom database connection URL (overrides individual connection parameters if provided) + #[clap(long)] + pub db_url: Option, + /// PostgreSQL schema search path (default is "$user,public") https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH #[clap(long)] pub pg_search_path: Option, diff --git a/src/common/config.rs b/src/common/config.rs index d7db20c6..02adc67f 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -40,16 +40,18 @@ pub struct GenerateTypesConfig { pub struct DbConnectionConfig { #[serde(rename = "DB_TYPE")] pub db_type: DatabaseType, - #[serde(rename = "DB_HOST")] + #[serde(rename = "DB_HOST", default)] pub db_host: String, - #[serde(rename = "DB_PORT")] + #[serde(rename = "DB_PORT", default)] pub db_port: u16, - #[serde(rename = "DB_USER")] + #[serde(rename = "DB_USER", default)] pub db_user: String, #[serde(rename = "DB_PASS")] pub db_pass: Option, #[serde(rename = "DB_NAME")] pub db_name: Option, + #[serde(rename = "DB_URL")] + pub db_url: Option, #[serde(rename = "PG_SEARCH_PATH")] pub pg_search_path: Option, #[serde(rename = "POOL_SIZE", default = "default_pool_size")] @@ -218,43 +220,73 @@ impl Config { panic!("") }); - let db_host = &CLI_ARGS - .db_host + let db_url = &CLI_ARGS + .db_url .clone() - .or_else(|| dotenv.db_host.clone()) - .or_else(|| default_config.map(|x| x.db_host.clone())) - .expect( + .or_else(|| dotenv.db_url.clone()) + .or_else(|| default_config.map(|x| x.db_url.clone()).flatten()); + + let db_host_chain = || { + CLI_ARGS + .db_host + .clone() + .or_else(|| dotenv.db_host.clone()) + .or_else(|| default_config.map(|x| x.db_host.clone())) + }; + + let db_host = match (db_url.is_some(), db_host_chain()) { + (true, Some(v)) => v, + (true, None) => String::new(), + (false, Some(v)) => v, + (false, None) => panic!( r" - Failed to fetch DB host. - Please provide it at least through a CLI arg or an environment variable or through - file based configuration - ", - ); - - let db_port = &CLI_ARGS - .db_port - .or(dotenv.db_port) - .or_else(|| default_config.map(|x| x.db_port)) - .expect( + Failed to fetch DB host. + Please provide it at least through a CLI arg or an environment variable or through + file based configuration, or provide a custom DB_URL + " + ), + }; + + let db_port_chain = || { + CLI_ARGS + .db_port + .or(dotenv.db_port) + .or_else(|| default_config.map(|x| x.db_port)) + }; + + let db_port = match (db_url.is_some(), db_port_chain()) { + (true, Some(v)) => v, + (true, None) => 0, + (false, Some(v)) => v, + (false, None) => panic!( r" - Failed to fetch DB port. - Please provide it at least through a CLI arg or an environment variable or through - file based configuration - ", - ); - - let db_user = &CLI_ARGS - .db_user - .clone() - .or_else(|| dotenv.db_user.clone()) - .or_else(|| default_config.map(|x| x.db_user.clone())) - .expect( + Failed to fetch DB port. + Please provide it at least through a CLI arg or an environment variable or through + file based configuration, or provide a custom DB_URL + " + ), + }; + + let db_user_chain = || { + CLI_ARGS + .db_user + .clone() + .or_else(|| dotenv.db_user.clone()) + .or_else(|| default_config.map(|x| x.db_user.clone())) + }; + + let db_user = match (db_url.is_some(), db_user_chain()) { + (true, Some(v)) => v, + (true, None) => String::new(), + (false, Some(v)) => v, + (false, None) => panic!( r" - Failed to fetch DB user. - Please provide it at least through a CLI arg or an environment variable or through - file based configuration - ", - ); + Failed to fetch DB user. + Please provide it at least through a CLI arg or an environment variable or through + file based configuration, or provide a custom DB_URL + " + ), + }; let db_pass = &CLI_ARGS .db_pass @@ -286,11 +318,12 @@ impl Config { DbConnectionConfig { db_type: db_type.to_owned(), - db_host: db_host.to_owned(), - db_port: db_port.to_owned(), - db_user: db_user.to_owned(), + db_host, + db_port, + db_user, db_pass: db_pass.to_owned(), db_name: db_name.to_owned(), + db_url: db_url.to_owned(), pg_search_path: pg_search_path.to_owned(), pool_size, connection_timeout, @@ -332,6 +365,11 @@ impl Config { /// This is to follow the spec of connection string for MySQL /// https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-reference-jdbc-url-format.html pub fn get_mysql_cred_str(&self, conn: &DbConnectionConfig) -> String { + // If custom DB_URL is provided, use it directly + if let Some(db_url) = &conn.db_url { + return db_url.to_owned(); + } + format!( "mysql://{user}:{pass}@{host}:{port}/{db_name}", user = &conn.db_user, @@ -344,6 +382,11 @@ impl Config { } pub fn get_postgres_cred(&self, conn: &DbConnectionConfig) -> String { + // If custom DB_URL is provided, use it directly + if let Some(db_url) = &conn.db_url { + return db_url.to_owned(); + } + format!( "postgresql://{user}:{pass}@{host}:{port}/{db_name}", user = &conn.db_user, diff --git a/src/common/dotenv.rs b/src/common/dotenv.rs index 8d45c2c6..17b65472 100644 --- a/src/common/dotenv.rs +++ b/src/common/dotenv.rs @@ -9,6 +9,7 @@ pub struct Dotenv { pub db_port: Option, pub db_pass: Option, pub db_name: Option, + pub db_url: Option, pub pg_search_path: Option, } @@ -44,6 +45,7 @@ impl Dotenv { db_port: Self::get_var("DB_PORT").map(|val| val.parse::().expect("DB_PORT is not a valid integer")), db_pass: Self::get_var("DB_PASS"), db_name: Self::get_var("DB_NAME"), + db_url: Self::get_var("DB_URL"), pg_search_path: Self::get_var("PG_SEARCH_PATH"), } } diff --git a/tests/cli.rs b/tests/cli.rs index ec25d9e9..b87f9483 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -225,4 +225,290 @@ mod cli_test { assert!(sample_query_path.exists()); Ok(()) } + + #[test] + fn postgres_db_url_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let sql_file_path = demo_path.join("test-query.sql"); + let sample_query_path = demo_path.join("test-query.queries.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo SQL file + let mut sql_file = fs::File::create(&sql_file_path)?; + writeln!(sql_file, "INSERT INTO items (name) VALUES ($1)")?; + + // Create config file with wrong DB settings (should be overridden by --db-url) + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "mysql", + "DB_HOST": "127.0.0.1", + "DB_PORT": 54321, + "DB_USER": "mysql", + "DB_PASS": "postgres", + "DB_NAME": "wrong" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())) + .arg("--db-type=postgres") + .arg("--db-url=postgres://postgres:postgres@localhost:54321/postgres"); + + cmd.assert().success(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } + + #[test] + fn postgres_db_url_from_config_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let sql_file_path = demo_path.join("test-query.sql"); + let sample_query_path = demo_path.join("test-query.queries.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo SQL file + let mut sql_file = fs::File::create(&sql_file_path)?; + writeln!(sql_file, "INSERT INTO items (name) VALUES ($1)")?; + + // Create config file with wrong DB settings (should be overridden by --db-url) + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_URL": "postgres://postgres:postgres@localhost:54321/postgres" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())); + + cmd.assert().success(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } + + #[test] + fn mysql_db_url_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let sql_file_path = demo_path.join("test-query.sql"); + let sample_query_path = demo_path.join("test-query.queries.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo SQL file (MySQL uses ? placeholders) + let mut sql_file = fs::File::create(&sql_file_path)?; + writeln!(sql_file, "INSERT INTO items (name) VALUES (?)")?; + + // Create config file with wrong DB settings (should be overridden by --db-url) + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_HOST": "127.0.0.1", + "DB_PORT": 33306, + "DB_USER": "postgres", + "DB_PASS": "root", + "DB_NAME": "wrong" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())) + .arg("--db-type=mysql") + .arg("--db-url=mysql://root@localhost:33306/sqlx-ts"); + + cmd.assert().success(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } + + #[test] + fn mysql_db_url_from_config_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let sql_file_path = demo_path.join("test-query.sql"); + let sample_query_path = demo_path.join("test-query.queries.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo SQL file (MySQL uses ? placeholders) + let mut sql_file = fs::File::create(&sql_file_path)?; + writeln!(sql_file, "INSERT INTO items (name) VALUES (?)")?; + + // Create config file with wrong DB settings (should be overridden by --db-url) + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_URL": "mysql://root@localhost:33306/sqlx-ts" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())); + + cmd.assert().success(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } + + #[test] + fn postgres_db_url_failure_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let ts_file_path = demo_path.join("test-query.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo TS file with SQL query + let mut ts_file = fs::File::create(&ts_file_path)?; + writeln!(ts_file, "const someQuery = sql`INSERT INTO items (name) VALUES ($1);`")?; + + // Create config file + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_HOST": "127.0.0.1", + "DB_PORT": 54321, + "DB_USER": "postgres", + "DB_PASS": "postgres", + "DB_NAME": "postgres" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE - Pass in wrong db-url that should fail + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())) + .arg("--db-type=postgres") + .arg("--db-url=postgres://wronguser:wrongpass@localhost:99999/wrongdb"); + + cmd.assert().failure(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } + + #[test] + fn mysql_db_url_failure_test() -> Result<(), Box> { + // SETUP + let demo_dir = tempdir()?; + let demo_path = demo_dir.path(); + let ts_file_path = demo_path.join("test-query.ts"); + + let config_dir = tempdir()?; + let config_file_path = config_dir.path().join(".sqlxrc.json"); + + // Create a demo TS file with SQL query (MySQL uses ? placeholders) + let mut ts_file = fs::File::create(&ts_file_path)?; + writeln!(ts_file, "const someQuery = sql`INSERT INTO items (name) VALUES (?);`")?; + + // Create config file + let mut config_file = fs::File::create(&config_file_path)?; + let config_content = r#" +{ + "generate_types": { + "enabled": false + }, + "connections": { + "default": { + "DB_TYPE": "mysql", + "DB_HOST": "127.0.0.1", + "DB_PORT": 33306, + "DB_USER": "root", + "DB_PASS": "", + "DB_NAME": "sqlx-ts" + } + } +}"#; + writeln!(config_file, "{config_content}")?; + + // EXECUTE - Pass in wrong db-url that should fail + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_file_path.to_str().unwrap())) + .arg("--db-type=mysql") + .arg("--db-url=mysql://wronguser:wrongpass@localhost:99999/wrongdb"); + + cmd.assert().failure(); + + // Cleanup happens automatically when demo_dir and config_dir go out of scope + Ok(()) + } } diff --git a/tests/sample/sample.queries.ts b/tests/sample/sample.queries.ts deleted file mode 100644 index f29c73b5..00000000 --- a/tests/sample/sample.queries.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type SampleSelectQueryParams = [number]; - -export interface ISampleSelectQueryResult { - name: string; - some_id: number; -} - -export interface ISampleSelectQueryQuery { - params: SampleSelectQueryParams; - result: ISampleSelectQueryResult; -} - -export type SampleInsertQueryParams = [string]; - -export interface ISampleInsertQueryResult { - -} - -export interface ISampleInsertQueryQuery { - params: SampleInsertQueryParams; - result: ISampleInsertQueryResult; -} - -export type SampleUpdateQueryParams = [string, number]; - -export interface ISampleUpdateQueryResult { - -} - -export interface ISampleUpdateQueryQuery { - params: SampleUpdateQueryParams; - result: ISampleUpdateQueryResult; -} - -export type SampleDeleteQueryParams = [number]; - -export interface ISampleDeleteQueryResult { - -} - -export interface ISampleDeleteQueryQuery { - params: SampleDeleteQueryParams; - result: ISampleDeleteQueryResult; -}