mirror of
https://github.com/getml/sqlgen.git
synced 2026-05-09 08:39:55 -05:00
Add parameterized execute for postgres (#122)
Support variadic execute with $1, $2, ... placeholders for safe parameter
binding. This allows calling PostgreSQL functions without defining custom
types.
Example:
conn->execute("SELECT my_func($1, $2)", arg1, arg2);
Supported types: string, numeric, bool, optional, nullptr/nullopt for NULL.
This commit is contained in:
+61
-6
@@ -2,9 +2,11 @@
|
||||
|
||||
The `sqlgen::postgres` module provides a type-safe and efficient interface for interacting with PostgreSQL databases. It implements the core database operations through a connection-based API with support for prepared statements, transactions, and efficient data iteration.
|
||||
|
||||
## Usage
|
||||
## Basic Usage
|
||||
|
||||
### Basic Connection
|
||||
This section describes the key aspects needed in order to use the module.
|
||||
|
||||
### Connection
|
||||
|
||||
Create a connection to a PostgreSQL database using credentials:
|
||||
|
||||
@@ -42,7 +44,7 @@ const auto query = sqlgen::read<std::vector<Person>> |
|
||||
const auto minors = query(conn);
|
||||
```
|
||||
|
||||
## Notes
|
||||
### Notes
|
||||
|
||||
- The module provides a type-safe interface for PostgreSQL operations
|
||||
- All operations return `sqlgen::Result<T>` for error handling
|
||||
@@ -60,6 +62,10 @@ const auto minors = query(conn);
|
||||
- Customizable connection parameters (host, port, database name, etc.)
|
||||
- LISTEN/NOTIFY for real-time event notifications
|
||||
|
||||
# Features
|
||||
|
||||
This section describes more advanced aspects of the `sqlgen::postgres` module, which may not be necessary for a typical user.
|
||||
|
||||
## LISTEN/NOTIFY
|
||||
|
||||
PostgreSQL provides a simple publish-subscribe mechanism through `LISTEN` and `NOTIFY` commands. This allows database clients to receive real-time notifications when events occur, without polling. Any client can send a notification to a channel, and all clients listening on that channel will receive it asynchronously.
|
||||
@@ -148,7 +154,6 @@ if (!result) {
|
||||
// Handle error...
|
||||
}
|
||||
```
|
||||
|
||||
## Notice Processor
|
||||
|
||||
PostgreSQL functions can emit NOTICE messages using `RAISE NOTICE` in PL/pgSQL. By default, libpq prints these to stderr. sqlgen allows you to capture these messages by providing a custom notice handler in the connection credentials.
|
||||
@@ -213,8 +218,7 @@ const auto creds = sqlgen::postgres::Credentials{
|
||||
|
||||
auto pool = sqlgen::make_connection_pool<sqlgen::postgres::Connection>(
|
||||
sqlgen::ConnectionPoolConfig{.size = 4},
|
||||
creds
|
||||
);
|
||||
creds);
|
||||
```
|
||||
|
||||
### Notes
|
||||
@@ -223,3 +227,54 @@ auto pool = sqlgen::make_connection_pool<sqlgen::postgres::Connection>(
|
||||
- The handler receives the full message including any trailing newline
|
||||
- The handler should be thread-safe when used with connection pools, as multiple connections may invoke it concurrently
|
||||
|
||||
## 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.
|
||||
|
||||
*Note*: using parameterized queries in this manner is highly discouraged within `sqlgen`, and should be used only as a last resort. You should consider first using the type-safe API. However, there are cases where this is useful such as when calling stored procedures that do not return results.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```cpp
|
||||
auto conn = sqlgen::postgres::connect(creds);
|
||||
if (!conn) {
|
||||
// Handle error...
|
||||
return;
|
||||
}
|
||||
|
||||
// Call a stored function with parameters
|
||||
auto result = (*conn)->execute(
|
||||
"SELECT provision_tenant($1, $2)",
|
||||
tenant_id,
|
||||
user_email
|
||||
);
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -32,6 +32,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()));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "sqlgen/postgres/PostgresV2Connection.hpp"
|
||||
#include "sqlgen/postgres/PostgresV2Result.hpp"
|
||||
|
||||
namespace sqlgen::postgres {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user