From eb3302c1ef0a2efe2f6edc7126a306cf1f8528c1 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Wed, 31 Dec 2025 00:39:21 +1100 Subject: [PATCH 1/3] add tests for exit code --- src/core/execute.rs | 3 +- tests/exit_code_on_error.rs | 277 ++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 tests/exit_code_on_error.rs diff --git a/src/core/execute.rs b/src/core/execute.rs index 37682b73..f2016bdc 100644 --- a/src/core/execute.rs +++ b/src/core/execute.rs @@ -26,7 +26,8 @@ pub async fn execute(queries: &HashMap>, handler: &Handler) -> let (explain_failed, ts_query) = &connection.prepare(sql, should_generate_types, handler).await?; // If any prepare statement fails, we should set the failed flag as true - failed = *explain_failed; + // Use OR to accumulate failures - once failed, it stays failed + failed = failed || *explain_failed; if *should_generate_types { let ts_query = &ts_query.clone().expect("Failed to generate types from query"); diff --git a/tests/exit_code_on_error.rs b/tests/exit_code_on_error.rs new file mode 100644 index 00000000..86f0a84e --- /dev/null +++ b/tests/exit_code_on_error.rs @@ -0,0 +1,277 @@ +#[cfg(test)] +mod exit_code_tests { + use assert_cmd::prelude::*; + use std::fs; + use std::io::Write; + use std::process::Command; + use tempfile::tempdir; + + /// This test verifies that even when there are multiple SQL queries with mixed + /// success/failure, the tool still exits with code 1 + #[test] + fn should_exit_with_code_1_when_sql_errors_detected() -> Result<(), Box> { + // SETUP + let dir = tempdir()?; + let parent_path = dir.path(); + let file_path = parent_path.join("index.ts"); + + let index_content = r#" +import { sql } from "sqlx-ts"; + +// This should succeed +const validQuery = sql`SELECT id, name FROM characters WHERE id = $1;`; + +// This should fail - unknown table +const invalidQuery = sql`SELECT * FROM unknown_table;`; + +// Another valid query after the failure +const anotherValidQuery = sql`SELECT * FROM inventory WHERE character_id = $1;`; + "#; + let mut temp_file = fs::File::create(&file_path)?; + writeln!(temp_file, "{}", index_content)?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=postgres") + .arg("--db-host=127.0.0.1") + .arg("--db-port=54321") + .arg("--db-user=postgres") + .arg("--db-pass=postgres") + .arg("--db-name=postgres"); + + // ASSERT - should exit with non-zero code due to the error + cmd + .assert() + .failure() + .stderr(predicates::str::contains("relation \"unknown_table\" does not exist")) + .stderr(predicates::str::contains("SQLs failed to compile!")); + + Ok(()) + } + + /// Test that when all queries succeed, exit code is 0 + #[test] + fn should_exit_with_code_0_when_no_errors() -> Result<(), Box> { + // SETUP + let dir = tempdir()?; + let parent_path = dir.path(); + let file_path = parent_path.join("valid.ts"); + + let index_content = r#" +import { sql } from "sqlx-ts"; + +const query1 = sql`SELECT id, name FROM characters WHERE id = $1;`; +const query2 = sql`SELECT * FROM inventory WHERE character_id = $1;`; +const query3 = sql`SELECT * FROM items WHERE id = $1;`; + "#; + let mut temp_file = fs::File::create(&file_path)?; + writeln!(temp_file, "{}", index_content)?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=postgres") + .arg("--db-host=127.0.0.1") + .arg("--db-port=54321") + .arg("--db-user=postgres") + .arg("--db-pass=postgres") + .arg("--db-name=postgres"); + + // ASSERT - should succeed + cmd + .assert() + .success() + .stdout(predicates::str::contains("No SQL errors detected!")); + + Ok(()) + } + + /// Test with many successes and one failure in the middle + /// Pattern: 5 successes -> 1 failure -> 5 successes + /// Expected: exit code 1 + #[test] + fn should_fail_with_many_successes_and_one_failure_in_middle( + ) -> Result<(), Box> { + // SETUP + let dir = tempdir()?; + let parent_path = dir.path(); + let file_path = parent_path.join("one_failure.ts"); + + let index_content = r#" +import { sql } from "sqlx-ts"; + +const query1 = sql`SELECT id FROM characters WHERE id = $1;`; +const query2 = sql`SELECT name FROM characters WHERE id = $1;`; +const query3 = sql`SELECT * FROM inventory WHERE id = $1;`; +const query4 = sql`SELECT * FROM items WHERE id = $1;`; +const query5 = sql`SELECT quantity FROM inventory WHERE id = $1;`; + +// Single failure in the middle +const failedQuery = sql`SELECT * FROM this_table_does_not_exist;`; + +const query6 = sql`SELECT rarity FROM items WHERE id = $1;`; +const query7 = sql`SELECT character_id FROM inventory WHERE id = $1;`; +const query8 = sql`SELECT flavor_text FROM items WHERE id = $1;`; +const query9 = sql`SELECT id, quantity FROM inventory WHERE character_id = $1;`; +const query10 = sql`SELECT id, name FROM characters LIMIT 10;`; + "#; + let mut temp_file = fs::File::create(&file_path)?; + writeln!(temp_file, "{}", index_content)?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=postgres") + .arg("--db-host=127.0.0.1") + .arg("--db-port=54321") + .arg("--db-user=postgres") + .arg("--db-pass=postgres") + .arg("--db-name=postgres"); + + // ASSERT - should fail despite 10 successes and only 1 failure + cmd + .assert() + .failure() + .stderr(predicates::str::contains( + "relation \"this_table_does_not_exist\" does not exist", + )) + .stderr(predicates::str::contains("SQLs failed to compile!")); + + Ok(()) + } + + /// Test with multiple files: one success file and one failure file + /// Expected: exit code 1 + #[test] + fn should_fail_with_multiple_files_one_success_one_failure( + ) -> Result<(), Box> { + // SETUP + let dir = tempdir()?; + let parent_path = dir.path(); + + // File 1: All successful queries + let file1_path = parent_path.join("success.ts"); + let file1_content = r#" +import { sql } from "sqlx-ts"; + +const query1 = sql`SELECT id, name FROM characters WHERE id = $1;`; +const query2 = sql`SELECT * FROM inventory WHERE character_id = $1;`; +const query3 = sql`SELECT * FROM items WHERE id = $1;`; + "#; + let mut file1 = fs::File::create(&file1_path)?; + writeln!(file1, "{}", file1_content)?; + + // File 2: Contains failures + let file2_path = parent_path.join("failure.ts"); + let file2_content = r#" +import { sql } from "sqlx-ts"; + +const failQuery1 = sql`SELECT * FROM nonexistent_table;`; +const failQuery2 = sql`SELECT * FROM another_missing_table;`; + "#; + let mut file2 = fs::File::create(&file2_path)?; + writeln!(file2, "{}", file2_content)?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=postgres") + .arg("--db-host=127.0.0.1") + .arg("--db-port=54321") + .arg("--db-user=postgres") + .arg("--db-pass=postgres") + .arg("--db-name=postgres"); + + // ASSERT - should fail because file2 has errors + cmd + .assert() + .failure() + .stderr(predicates::str::contains( + "relation \"nonexistent_table\" does not exist", + )) + .stderr(predicates::str::contains( + "relation \"another_missing_table\" does not exist", + )) + .stderr(predicates::str::contains("SQLs failed to compile!")); + + Ok(()) + } + + /// Test with multiple files: all files contain successful queries + /// Expected: exit code 0 + #[test] + fn should_succeed_with_multiple_files_all_successful() -> Result<(), Box> + { + // SETUP + let dir = tempdir()?; + let parent_path = dir.path(); + + // File 1: Successful queries + let file1_path = parent_path.join("queries1.ts"); + let file1_content = r#" +import { sql } from "sqlx-ts"; + +const query1 = sql`SELECT id FROM characters WHERE id = $1;`; +const query2 = sql`SELECT name FROM characters WHERE id = $1;`; + "#; + let mut file1 = fs::File::create(&file1_path)?; + writeln!(file1, "{}", file1_content)?; + + // File 2: More successful queries + let file2_path = parent_path.join("queries2.ts"); + let file2_content = r#" +import { sql } from "sqlx-ts"; + +const query3 = sql`SELECT * FROM inventory WHERE id = $1;`; +const query4 = sql`SELECT * FROM items WHERE id = $1;`; + "#; + let mut file2 = fs::File::create(&file2_path)?; + writeln!(file2, "{}", file2_content)?; + + // File 3: Even more successful queries + let file3_path = parent_path.join("queries3.ts"); + let file3_content = r#" +import { sql } from "sqlx-ts"; + +const query5 = sql`SELECT quantity FROM inventory WHERE character_id = $1;`; +const query6 = sql`SELECT rarity FROM items WHERE id = $1;`; + "#; + let mut file3 = fs::File::create(&file3_path)?; + writeln!(file3, "{}", file3_content)?; + + // EXECUTE + let mut cmd = Command::cargo_bin("sqlx-ts").unwrap(); + + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=postgres") + .arg("--db-host=127.0.0.1") + .arg("--db-port=54321") + .arg("--db-user=postgres") + .arg("--db-pass=postgres") + .arg("--db-name=postgres"); + + // ASSERT - should succeed + cmd + .assert() + .success() + .stdout(predicates::str::contains("No SQL errors detected!")); + + Ok(()) + } +} + From 8954f2fbbc4ceca36f2b55a88eafb46b3412dbb8 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Wed, 31 Dec 2025 00:42:47 +1100 Subject: [PATCH 2/3] fmt --- tests/exit_code_on_error.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/exit_code_on_error.rs b/tests/exit_code_on_error.rs index 86f0a84e..780d0509 100644 --- a/tests/exit_code_on_error.rs +++ b/tests/exit_code_on_error.rs @@ -97,8 +97,7 @@ const query3 = sql`SELECT * FROM items WHERE id = $1;`; /// Pattern: 5 successes -> 1 failure -> 5 successes /// Expected: exit code 1 #[test] - fn should_fail_with_many_successes_and_one_failure_in_middle( - ) -> Result<(), Box> { + fn should_fail_with_many_successes_and_one_failure_in_middle() -> Result<(), Box> { // SETUP let dir = tempdir()?; let parent_path = dir.path(); @@ -153,8 +152,7 @@ const query10 = sql`SELECT id, name FROM characters LIMIT 10;`; /// Test with multiple files: one success file and one failure file /// Expected: exit code 1 #[test] - fn should_fail_with_multiple_files_one_success_one_failure( - ) -> Result<(), Box> { + fn should_fail_with_multiple_files_one_success_one_failure() -> Result<(), Box> { // SETUP let dir = tempdir()?; let parent_path = dir.path(); @@ -213,8 +211,7 @@ const failQuery2 = sql`SELECT * FROM another_missing_table;`; /// Test with multiple files: all files contain successful queries /// Expected: exit code 0 #[test] - fn should_succeed_with_multiple_files_all_successful() -> Result<(), Box> - { + fn should_succeed_with_multiple_files_all_successful() -> Result<(), Box> { // SETUP let dir = tempdir()?; let parent_path = dir.path(); @@ -274,4 +271,3 @@ const query6 = sql`SELECT rarity FROM items WHERE id = $1;`; Ok(()) } } - From 1eb815ffadee6dddd8242d46f1758b1233ba0b84 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Wed, 31 Dec 2025 00:57:01 +1100 Subject: [PATCH 3/3] fix wrong query --- tests/demo/select/select.queries.ts | 4 ++-- tests/demo/select/select.snapshot.ts | 4 ++-- tests/demo/select/select.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/demo/select/select.queries.ts b/tests/demo/select/select.queries.ts index 6009c1d3..1f58479d 100644 --- a/tests/demo/select/select.queries.ts +++ b/tests/demo/select/select.queries.ts @@ -240,7 +240,7 @@ export interface ISelectSql20Query { export type SelectSql21Params = []; export interface ISelectSql21Result { - desc: string; + description: string; } export interface ISelectSql21Query { @@ -306,7 +306,7 @@ export interface ISelectSql26Query { export type SelectSql27Params = []; export interface ISelectSql27Result { - rank: string; + guildRank: string; } export interface ISelectSql27Query { diff --git a/tests/demo/select/select.snapshot.ts b/tests/demo/select/select.snapshot.ts index 5ddd0fe3..37fb9cfa 100644 --- a/tests/demo/select/select.snapshot.ts +++ b/tests/demo/select/select.snapshot.ts @@ -240,7 +240,7 @@ export interface ISelectSql20Query { export type SelectSql21Params = []; export interface ISelectSql21Result { - desc: string; + description: string; } export interface ISelectSql21Query { @@ -306,7 +306,7 @@ export interface ISelectSql26Query { export type SelectSql27Params = []; export interface ISelectSql27Result { - rank: string; + guildRank: string; } export interface ISelectSql27Query { diff --git a/tests/demo/select/select.ts b/tests/demo/select/select.ts index f06cc4d2..e8ea2963 100644 --- a/tests/demo/select/select.ts +++ b/tests/demo/select/select.ts @@ -113,7 +113,7 @@ SELECT IFNULL(gold, 0.0) AS gold_amount FROM characters; // SELECT IFNULL with TEXT type const selectSql21 = sql` -- @db: db_mysql -SELECT IFNULL(description, 'No description') AS desc FROM factions; +SELECT IFNULL(description, 'No description') AS description FROM factions; ` // SELECT COALESCE with multiple arguments @@ -149,7 +149,7 @@ SELECT NULLIF(quantity, 0) AS inv_quantity FROM inventory; // SELECT NVL with VARCHAR (Oracle-style, but should work) const selectSql27 = sql` -- @db: db_mysql -SELECT NVL(guild_rank, 'Member') AS rank FROM guild_members; +SELECT IFNULL(guild_rank, 'Member') AS guild_rank FROM guild_members; ` // SELECT IFNULL with compound identifier