diff --git a/docs/postgres.md b/docs/postgres.md index 3374065..88e85c2 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -149,3 +149,66 @@ if (!result) { } ``` +## Parameterized Queries + +The `execute` method supports parameterized queries using PostgreSQL's `$1, $2, ...` placeholder syntax. This prevents SQL injection and allows safe execution of dynamic queries without needing to define custom types. + +### Basic Usage + +```cpp +auto conn = sqlgen::postgres::connect(creds); +if (!conn) { + // Handle error... + return; +} + +// Execute with string and integer parameters +auto result = (*conn)->execute( + "INSERT INTO users (name, age) VALUES ($1, $2)", + std::string("Alice"), + 30 +); +``` + +### Supported Parameter Types + +The following types are automatically converted to SQL parameters: + +- `std::string` - passed as-is +- `const char*` / `char*` - converted to string (nullptr becomes NULL) +- Numeric types (`int`, `long`, `double`, etc.) - converted via `std::to_string` +- `bool` - converted to `"true"` or `"false"` +- `std::optional` - value or NULL if `std::nullopt` +- `std::nullopt` / `nullptr` - NULL value + +### Calling PostgreSQL Functions + +This is particularly useful for calling PL/pgSQL functions without needing to define wrapper types: + +```cpp +// Call a stored function with parameters +auto result = (*conn)->execute( + "SELECT provision_tenant($1, $2)", + tenant_id, + user_email +); +``` + +### Handling NULL Values + +Use `std::optional` or `std::nullopt` to pass NULL values: + +```cpp +std::optional maybe_value = std::nullopt; +auto result = (*conn)->execute( + "INSERT INTO data (nullable_field) VALUES ($1)", + maybe_value +); +``` + +### Notes + +- Parameters are sent in text format and type inference is handled by PostgreSQL +- This feature uses `PQexecParams` internally for safe parameter binding +- The original `execute(sql)` overload without parameters remains available + diff --git a/include/sqlgen/postgres/Connection.hpp b/include/sqlgen/postgres/Connection.hpp index 45d6d58..244ba97 100644 --- a/include/sqlgen/postgres/Connection.hpp +++ b/include/sqlgen/postgres/Connection.hpp @@ -67,6 +67,45 @@ class SQLGEN_API Connection { Result execute(const std::string& _sql) noexcept; + template + Result execute(const std::string& _sql, Args&&... _args) noexcept { + return execute_params(_sql, {to_param(std::forward(_args))...}); + } + + private: + template + static std::optional to_param(const T& _val) { + if constexpr (std::is_same_v, std::nullopt_t>) { + return std::nullopt; + } else if constexpr (std::is_same_v, std::nullptr_t>) { + return std::nullopt; + } else if constexpr (std::is_same_v, std::string>) { + return _val; + } else if constexpr (std::is_same_v, const char*> || + std::is_same_v, char*>) { + return _val ? std::optional(_val) : std::nullopt; + } else if constexpr (std::is_same_v, bool>) { + return _val ? "true" : "false"; + } else if constexpr (std::is_arithmetic_v>) { + return std::to_string(_val); + } else { + static_assert(std::is_convertible_v, + "Parameter type must be convertible to string"); + return std::string(_val); + } + } + + template + static std::optional to_param(const std::optional& _val) { + return _val ? to_param(*_val) : std::nullopt; + } + + Result execute_params( + const std::string& _sql, + const std::vector>& _params) noexcept; + + public: + template Result insert(const dynamic::Insert& _stmt, ItBegin _begin, ItEnd _end) noexcept { diff --git a/include/sqlgen/postgres/PostgresV2Result.hpp b/include/sqlgen/postgres/PostgresV2Result.hpp index f9895c4..37dcbbe 100644 --- a/include/sqlgen/postgres/PostgresV2Result.hpp +++ b/include/sqlgen/postgres/PostgresV2Result.hpp @@ -25,6 +25,10 @@ class SQLGEN_API PostgresV2Result { static rfl::Result make( const std::string& _query, const PostgresV2Connection& _conn) noexcept; + static rfl::Result make( + const std::string& _query, const PostgresV2Connection& _conn, + const std::vector>& _params) noexcept; + static rfl::Result make(PGresult* _ptr) noexcept { try { return PostgresV2Result(_ptr); diff --git a/src/sqlgen/postgres/Connection.cpp b/src/sqlgen/postgres/Connection.cpp index ee74c5b..8901515 100644 --- a/src/sqlgen/postgres/Connection.cpp +++ b/src/sqlgen/postgres/Connection.cpp @@ -30,6 +30,14 @@ Result Connection::execute(const std::string& _sql) noexcept { }); } +Result Connection::execute_params( + const std::string& _sql, + const std::vector>& _params) noexcept { + return PostgresV2Result::make(_sql, conn_, _params).transform([](auto&&) { + return Nothing{}; + }); +} + Result Connection::end_write() { if (PQputCopyEnd(conn_.ptr(), NULL) == -1) { return error(PQerrorMessage(conn_.ptr())); diff --git a/src/sqlgen/postgres/PostgresV2Result.cpp b/src/sqlgen/postgres/PostgresV2Result.cpp index c6b7c7c..c7bd1bb 100644 --- a/src/sqlgen/postgres/PostgresV2Result.cpp +++ b/src/sqlgen/postgres/PostgresV2Result.cpp @@ -1,4 +1,5 @@ #include "sqlgen/postgres/PostgresV2Connection.hpp" +#include "sqlgen/postgres/PostgresV2Result.hpp" namespace sqlgen::postgres { @@ -16,4 +17,31 @@ rfl::Result PostgresV2Result::make( return PostgresV2Result(res); } +rfl::Result PostgresV2Result::make( + const std::string& _query, const PostgresV2Connection& _conn, + const std::vector>& _params) noexcept { + std::vector param_values(_params.size()); + for (size_t i = 0; i < _params.size(); ++i) { + param_values[i] = _params[i] ? _params[i]->c_str() : nullptr; + } + + auto res = PQexecParams(_conn.ptr(), _query.c_str(), + static_cast(_params.size()), + nullptr, // paramTypes (let server infer) + param_values.data(), // paramValues + nullptr, // paramLengths (text format) + nullptr, // paramFormats (text format) + 0); // resultFormat (text) + + const auto status = PQresultStatus(res); + if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && + status != PGRES_COPY_IN) { + const auto msg = + std::string("Query execution failed: ") + PQerrorMessage(_conn.ptr()); + PQclear(res); + return error(msg); + } + return PostgresV2Result(res); +} + } // namespace sqlgen::postgres diff --git a/tests/postgres/test_execute_params.cpp b/tests/postgres/test_execute_params.cpp new file mode 100644 index 0000000..d576f8d --- /dev/null +++ b/tests/postgres/test_execute_params.cpp @@ -0,0 +1,141 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include + +namespace test_execute_params { + +TEST(postgres, execute_with_string_params) { + const auto credentials = sqlgen::postgres::Credentials{ + .user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + auto conn_result = sqlgen::postgres::connect(credentials); + ASSERT_TRUE(conn_result); + auto conn = conn_result.value(); + + // Create a test table + auto create_result = conn->execute(R"( + CREATE TABLE IF NOT EXISTS test_execute_params ( + id SERIAL PRIMARY KEY, + name TEXT, + value INTEGER + ); + )"); + ASSERT_TRUE(create_result) << create_result.error().what(); + + // Clean up any existing data + auto truncate_result = conn->execute("TRUNCATE test_execute_params;"); + ASSERT_TRUE(truncate_result) << truncate_result.error().what(); + + // Insert using parameterized execute + auto insert_result = conn->execute( + "INSERT INTO test_execute_params (name, value) VALUES ($1, $2);", + std::string("test_name"), 42); + ASSERT_TRUE(insert_result) << insert_result.error().what(); + + // Clean up + auto drop_result = conn->execute("DROP TABLE test_execute_params;"); + ASSERT_TRUE(drop_result) << drop_result.error().what(); +} + +TEST(postgres, execute_with_null_param) { + const auto credentials = sqlgen::postgres::Credentials{ + .user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + auto conn_result = sqlgen::postgres::connect(credentials); + ASSERT_TRUE(conn_result); + auto conn = conn_result.value(); + + // Create a test table + auto create_result = conn->execute(R"( + CREATE TABLE IF NOT EXISTS test_execute_null ( + id SERIAL PRIMARY KEY, + name TEXT + ); + )"); + ASSERT_TRUE(create_result) << create_result.error().what(); + + // Insert with null parameter using std::optional + std::optional null_val = std::nullopt; + auto insert_result = conn->execute( + "INSERT INTO test_execute_null (name) VALUES ($1);", null_val); + ASSERT_TRUE(insert_result) << insert_result.error().what(); + + // Clean up + auto drop_result = conn->execute("DROP TABLE test_execute_null;"); + ASSERT_TRUE(drop_result) << drop_result.error().what(); +} + +TEST(postgres, execute_with_numeric_params) { + const auto credentials = sqlgen::postgres::Credentials{ + .user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + auto conn_result = sqlgen::postgres::connect(credentials); + ASSERT_TRUE(conn_result); + auto conn = conn_result.value(); + + // Create a test table + auto create_result = conn->execute(R"( + CREATE TABLE IF NOT EXISTS test_execute_numeric ( + id SERIAL PRIMARY KEY, + int_val INTEGER, + float_val DOUBLE PRECISION, + bool_val BOOLEAN + ); + )"); + ASSERT_TRUE(create_result) << create_result.error().what(); + + // Insert with various numeric types + auto insert_result = conn->execute( + "INSERT INTO test_execute_numeric (int_val, float_val, bool_val) " + "VALUES ($1, $2, $3);", + 123, 3.14159, true); + ASSERT_TRUE(insert_result) << insert_result.error().what(); + + // Clean up + auto drop_result = conn->execute("DROP TABLE test_execute_numeric;"); + ASSERT_TRUE(drop_result) << drop_result.error().what(); +} + +TEST(postgres, execute_call_function) { + const auto credentials = sqlgen::postgres::Credentials{ + .user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + auto conn_result = sqlgen::postgres::connect(credentials); + ASSERT_TRUE(conn_result); + auto conn = conn_result.value(); + + // Create a simple test function + auto create_fn_result = conn->execute(R"( + CREATE OR REPLACE FUNCTION test_add(a INTEGER, b INTEGER) + RETURNS INTEGER AS $$ + BEGIN + RETURN a + b; + END; + $$ LANGUAGE plpgsql; + )"); + ASSERT_TRUE(create_fn_result) << create_fn_result.error().what(); + + // Call the function with parameters + auto call_result = conn->execute("SELECT test_add($1, $2);", 5, 3); + ASSERT_TRUE(call_result) << call_result.error().what(); + + // Clean up + auto drop_fn_result = conn->execute("DROP FUNCTION test_add;"); + ASSERT_TRUE(drop_fn_result) << drop_fn_result.error().what(); +} + +} // namespace test_execute_params + +#endif