Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` - 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<std::string> 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

39 changes: 39 additions & 0 deletions include/sqlgen/postgres/Connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ class SQLGEN_API Connection {

Result<Nothing> execute(const std::string& _sql) noexcept;

template <class... Args>
Result<Nothing> execute(const std::string& _sql, Args&&... _args) noexcept {
return execute_params(_sql, {to_param(std::forward<Args>(_args))...});
}

private:
template <class T>
static std::optional<std::string> to_param(const T& _val) {
if constexpr (std::is_same_v<std::decay_t<T>, std::nullopt_t>) {
return std::nullopt;
} else if constexpr (std::is_same_v<std::decay_t<T>, std::nullptr_t>) {
return std::nullopt;
} else if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
return _val;
} else if constexpr (std::is_same_v<std::decay_t<T>, const char*> ||
std::is_same_v<std::decay_t<T>, char*>) {
return _val ? std::optional<std::string>(_val) : std::nullopt;
} else if constexpr (std::is_same_v<std::decay_t<T>, bool>) {
return _val ? "true" : "false";
} else if constexpr (std::is_arithmetic_v<std::decay_t<T>>) {
return std::to_string(_val);
} else {
static_assert(std::is_convertible_v<T, std::string>,
"Parameter type must be convertible to string");
return std::string(_val);
}
}

template <class T>
static std::optional<std::string> to_param(const std::optional<T>& _val) {
return _val ? to_param(*_val) : std::nullopt;
}

Result<Nothing> execute_params(
const std::string& _sql,
const std::vector<std::optional<std::string>>& _params) noexcept;

public:

template <class ItBegin, class ItEnd>
Result<Nothing> insert(const dynamic::Insert& _stmt, ItBegin _begin,
ItEnd _end) noexcept {
Expand Down
4 changes: 4 additions & 0 deletions include/sqlgen/postgres/PostgresV2Result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class SQLGEN_API PostgresV2Result {
static rfl::Result<PostgresV2Result> make(
const std::string& _query, const PostgresV2Connection& _conn) noexcept;

static rfl::Result<PostgresV2Result> make(
const std::string& _query, const PostgresV2Connection& _conn,
const std::vector<std::optional<std::string>>& _params) noexcept;

static rfl::Result<PostgresV2Result> make(PGresult* _ptr) noexcept {
try {
return PostgresV2Result(_ptr);
Expand Down
8 changes: 8 additions & 0 deletions src/sqlgen/postgres/Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ Result<Nothing> Connection::execute(const std::string& _sql) noexcept {
});
}

Result<Nothing> Connection::execute_params(
const std::string& _sql,
const std::vector<std::optional<std::string>>& _params) noexcept {
return PostgresV2Result::make(_sql, conn_, _params).transform([](auto&&) {
return Nothing{};
});
}

Result<Nothing> Connection::end_write() {
if (PQputCopyEnd(conn_.ptr(), NULL) == -1) {
return error(PQerrorMessage(conn_.ptr()));
Expand Down
28 changes: 28 additions & 0 deletions src/sqlgen/postgres/PostgresV2Result.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "sqlgen/postgres/PostgresV2Connection.hpp"
#include "sqlgen/postgres/PostgresV2Result.hpp"

namespace sqlgen::postgres {

Expand All @@ -16,4 +17,31 @@ rfl::Result<PostgresV2Result> PostgresV2Result::make(
return PostgresV2Result(res);
}

rfl::Result<PostgresV2Result> PostgresV2Result::make(
const std::string& _query, const PostgresV2Connection& _conn,
const std::vector<std::optional<std::string>>& _params) noexcept {
std::vector<const char*> 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<int>(_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
141 changes: 141 additions & 0 deletions tests/postgres/test_execute_params.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY

#include <gtest/gtest.h>

#include <optional>
#include <sqlgen/postgres.hpp>
#include <string>

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<std::string> 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
Loading