Skip to content

Commit 4fe38f4

Browse files
[dbtool] Add exec <QUERY> command for ad-hoc SQL queries
Thin diagnostics helper for inspecting INFORMATION_SCHEMA, sanity- checking row counts, or running one-off queries from CI / shell scripts. Streams the result set as tab-separated values to stdout; row count goes to stderr. The query is read from the command-line argument or, when no argument is supplied (or `-` is passed), from stdin until EOF. Multi-statement scripts pass through to ExecuteDirect — the ODBC driver advances through subsequent result sets automatically. dbtool --profile X exec "SELECT * FROM INFORMATION_SCHEMA.COLUMNS" echo "SELECT COUNT(*) FROM users" | dbtool --profile X exec Used during the MSSQL UTF-8 literal investigation to confirm that MSSQL was decoding raw `N'für'` as four CP-1252 codepoints rather than three Unicode codepoints, which led to the NCHAR-concatenation fix shipped in the previous commit. Signed-off-by: Christian Parpart <christian@parpart.family>
1 parent e6db799 commit 4fe38f4

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

src/tools/dbtool/main.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ void PrintUsage()
141141
std::println(" the post-migration state. No DB connection required.");
142142
std::println(" For .sql output, --dialect is required (sqlite, postgres,");
143143
std::println(" mssql, mysql).");
144+
std::println(" {}exec{} {}<QUERY>{} Executes the given SQL query and prints any result set.",
145+
c.command, c.reset, c.param, c.reset);
146+
std::println(" Pass `-` (or omit the argument) to read the query from stdin.");
144147
std::println(" {}backup{} --output FILE Backs up the database to a file", c.command, c.reset);
145148
std::println(" {}restore{} --input FILE Restores the database from a file", c.command, c.reset);
146149
std::println(" {}resolve-secret{} {}<REF>{} Prints a resolved secret to stdout (env:, file:, stdin:)",
@@ -1969,6 +1972,84 @@ int Restore(Options const& options)
19691972
return pm->ErrorCount() > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
19701973
}
19711974

1975+
/// @brief Reads a SQL query from the command argument or, when no argument was
1976+
/// supplied (or `-` was passed), from stdin until EOF. Stripped of trailing
1977+
/// whitespace so a stray newline doesn't reach the driver.
1978+
[[nodiscard]] std::string ResolveExecQueryText(std::string const& argument)
1979+
{
1980+
std::string source;
1981+
if (argument.empty() || argument == "-")
1982+
{
1983+
std::string const stdinContent { std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>() };
1984+
source = stdinContent;
1985+
}
1986+
else
1987+
{
1988+
source = argument;
1989+
}
1990+
while (!source.empty() && (source.back() == '\n' || source.back() == '\r' || source.back() == ' ' || source.back() == '\t'))
1991+
source.pop_back();
1992+
return source;
1993+
}
1994+
1995+
/// @brief Executes a single SQL statement against the configured connection and
1996+
/// streams the result set (if any) to stdout in a tab-separated layout.
1997+
///
1998+
/// Multi-statement scripts are supported by issuing the whole text as a single
1999+
/// `ExecuteDirect` call — the ODBC driver advances through any subsequent result
2000+
/// sets automatically. Empty result sets simply print a row count line.
2001+
///
2002+
/// Useful as a thin diagnostics helper (e.g. inspecting `INFORMATION_SCHEMA`)
2003+
/// from CI / shell scripts. Reads the query from `--argument` or, when no
2004+
/// argument is supplied, from stdin.
2005+
int ExecQuery(Options const& options)
2006+
{
2007+
auto const queryText = ResolveExecQueryText(options.argument);
2008+
if (queryText.empty())
2009+
{
2010+
std::println(std::cerr, "Error: exec requires a query — pass it as an argument or via stdin.");
2011+
return EXIT_FAILURE;
2012+
}
2013+
2014+
SqlConnection conn;
2015+
SqlStatement stmt(conn);
2016+
2017+
try
2018+
{
2019+
auto cursor = stmt.ExecuteDirect(queryText);
2020+
// Print column headers if the statement produced a result set.
2021+
auto const numColumns = cursor.NumColumnsAffected();
2022+
if (numColumns == 0)
2023+
{
2024+
std::println("(no result set)");
2025+
return EXIT_SUCCESS;
2026+
}
2027+
2028+
std::size_t rowCount = 0;
2029+
while (cursor.FetchRow())
2030+
{
2031+
for (size_t i = 1; i <= numColumns; ++i)
2032+
{
2033+
if (i != 1)
2034+
std::print("\t");
2035+
auto const value = cursor.GetNullableColumn<std::string>(static_cast<SQLUSMALLINT>(i));
2036+
std::print("{}", value.value_or(std::string { "(null)" }));
2037+
}
2038+
std::println("");
2039+
++rowCount;
2040+
}
2041+
std::println(std::cerr, "({} row{})", rowCount, rowCount == 1 ? "" : "s");
2042+
return EXIT_SUCCESS;
2043+
}
2044+
catch (SqlException const& ex)
2045+
{
2046+
std::println(std::cerr, "SQL error: {}", ex.info().message);
2047+
if (!ex.info().sqlState.empty() && ex.info().sqlState != " ")
2048+
std::println(std::cerr, " SQL State: {}, Native error: {}", ex.info().sqlState, ex.info().nativeErrorCode);
2049+
return EXIT_FAILURE;
2050+
}
2051+
}
2052+
19722053
MigrationManager& GetMigrationManager(Options const& options)
19732054
{
19742055
// Keep plugins loaded for the lifetime of the program.
@@ -2080,6 +2161,8 @@ int DispatchDbCommand(Options const& options)
20802161
return Backup(options);
20812162
if (options.command == "restore")
20822163
return Restore(options);
2164+
if (options.command == "exec")
2165+
return ExecQuery(options);
20832166

20842167
std::println(std::cerr, "Unknown command: {}", options.command);
20852168
return EXIT_FAILURE;

0 commit comments

Comments
 (0)