Skip to content
Merged
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
74 changes: 74 additions & 0 deletions docs/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,77 @@ if (!result) {
}
```

## 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.

### Setting Up a Notice Handler

```cpp
#include <sqlgen/postgres.hpp>
#include <iostream>

// Create credentials with a notice handler
const auto creds = sqlgen::postgres::Credentials{
.user = "myuser",
.password = "mypassword",
.host = "localhost",
.dbname = "mydatabase",
.notice_handler = [](const char* msg) {
std::cout << "[PostgreSQL Notice] " << msg;
}
};

auto conn = sqlgen::postgres::connect(creds);
```

### Integration with Logging Frameworks

The notice handler can forward messages to your preferred logging framework:

```cpp
#include <spdlog/spdlog.h>

const auto creds = sqlgen::postgres::Credentials{
.user = "myuser",
.password = "mypassword",
.host = "localhost",
.dbname = "mydatabase",
.notice_handler = [](const char* msg) {
// Remove trailing newline if present
std::string_view sv(msg);
if (!sv.empty() && sv.back() == '\n') {
sv.remove_suffix(1);
}
spdlog::info("PostgreSQL: {}", sv);
}
};
```

### Using with Connection Pools

The notice handler is set per-connection, so all connections in a pool will use the same handler:

```cpp
const auto creds = sqlgen::postgres::Credentials{
.user = "myuser",
.password = "mypassword",
.host = "localhost",
.dbname = "mydatabase",
.notice_handler = [](const char* msg) {
my_logger::log(msg);
}
};

auto pool = sqlgen::make_connection_pool<sqlgen::postgres::Connection>(
sqlgen::ConnectionPoolConfig{.size = 4},
creds
);
```

### Notes

- If no `notice_handler` is provided, libpq's default behavior (printing to stderr) is used
- 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

2 changes: 2 additions & 0 deletions include/sqlgen/postgres/Credentials.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef SQLGEN_POSTGRES_CREDENTIALS_HPP_
#define SQLGEN_POSTGRES_CREDENTIALS_HPP_

#include <functional>
#include <string>

namespace sqlgen::postgres {
Expand All @@ -11,6 +12,7 @@ struct Credentials {
std::string host;
std::string dbname;
int port = 5432;
std::function<void(const char*)> notice_handler;

std::string to_str() const {
return "postgresql://" + user + ":" + password + "@" + host + ":" +
Expand Down
9 changes: 9 additions & 0 deletions include/sqlgen/postgres/PostgresV2Connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <libpq-fe.h>

#include <functional>
#include <memory>
#include <rfl.hpp>
#include <stdexcept>
Expand All @@ -15,15 +16,22 @@ namespace sqlgen::postgres {

class SQLGEN_API PostgresV2Connection {
public:
using NoticeHandler = std::function<void(const char*)>;

PostgresV2Connection(PGconn* _ptr)
: ptr_(Ref<PGconn>::make(std::shared_ptr<PGconn>(_ptr, &PQfinish))
.value()) {}

PostgresV2Connection(PGconn* _ptr, NoticeHandler _notice_handler);

~PostgresV2Connection() = default;

static rfl::Result<PostgresV2Connection> make(
const std::string& _conn_str) noexcept;

static rfl::Result<PostgresV2Connection> make(
const std::string& _conn_str, NoticeHandler _notice_handler) noexcept;

static rfl::Result<PostgresV2Connection> make(PGconn* _ptr) noexcept {
try {
return PostgresV2Connection(_ptr);
Expand All @@ -37,6 +45,7 @@ class SQLGEN_API PostgresV2Connection {

private:
Ref<PGconn> ptr_;
std::shared_ptr<NoticeHandler> notice_handler_;
};

} // namespace sqlgen::postgres
Expand Down
7 changes: 5 additions & 2 deletions src/sqlgen/postgres/Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ namespace sqlgen::postgres {
Connection::Connection(const Conn& _conn) : conn_(_conn) {}

Connection::Connection(const Credentials& _credentials)
: conn_(PostgresV2Connection::make(_credentials.to_str()).value()) {}
: conn_(PostgresV2Connection::make(_credentials.to_str(),
_credentials.notice_handler)
.value()) {}

Result<Nothing> Connection::begin_transaction() noexcept {
return execute("BEGIN TRANSACTION;");
Expand Down Expand Up @@ -182,7 +184,8 @@ Result<Nothing> Connection::insert_impl(

rfl::Result<Ref<Connection>> Connection::make(
const Credentials& _credentials) noexcept {
return PostgresV2Connection::make(_credentials.to_str())
return PostgresV2Connection::make(_credentials.to_str(),
_credentials.notice_handler)
.transform([](auto&& _conn) { return Ref<Connection>::make(_conn); });
}

Expand Down
34 changes: 34 additions & 0 deletions src/sqlgen/postgres/PostgresV2Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

namespace sqlgen::postgres {

namespace {
void notice_trampoline(void* arg, const char* message) {
auto* handler =
static_cast<PostgresV2Connection::NoticeHandler*>(arg);
if (handler && *handler) {
(*handler)(message);
}
}
} // namespace

PostgresV2Connection::PostgresV2Connection(PGconn* _ptr,
NoticeHandler _notice_handler)
: ptr_(Ref<PGconn>::make(std::shared_ptr<PGconn>(_ptr, &PQfinish)).value()),
notice_handler_(_notice_handler
? std::make_shared<NoticeHandler>(
std::move(_notice_handler))
: nullptr) {
if (notice_handler_) {
PQsetNoticeProcessor(ptr_.get(), notice_trampoline, notice_handler_.get());
}
}

rfl::Result<PostgresV2Connection> PostgresV2Connection::make(
const std::string& _conn_str) noexcept {
auto conn = PQconnectdb(_conn_str.c_str());
Expand All @@ -14,4 +36,16 @@ rfl::Result<PostgresV2Connection> PostgresV2Connection::make(
return PostgresV2Connection(conn);
}

rfl::Result<PostgresV2Connection> PostgresV2Connection::make(
const std::string& _conn_str, NoticeHandler _notice_handler) noexcept {
auto conn = PQconnectdb(_conn_str.c_str());
if (PQstatus(conn) != CONNECTION_OK) {
const auto msg =
std::string("Connection to postgres failed: ") + PQerrorMessage(conn);
PQfinish(conn);
return error(msg);
}
return PostgresV2Connection(conn, std::move(_notice_handler));
}

} // namespace sqlgen::postgres
109 changes: 109 additions & 0 deletions tests/postgres/test_notice_processor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY

#include <gtest/gtest.h>

#include <mutex>
#include <sqlgen/postgres.hpp>
#include <string>
#include <vector>

namespace test_notice_processor {

TEST(postgres, notice_processor_captures_raise_notice) {
std::vector<std::string> captured_notices;
std::mutex mtx;

const auto credentials = sqlgen::postgres::Credentials{
.user = "postgres",
.password = "password",
.host = "localhost",
.dbname = "postgres",
.notice_handler = [&](const char* msg) {
std::lock_guard<std::mutex> lock(mtx);
captured_notices.push_back(msg);
}};

auto conn_result = sqlgen::postgres::connect(credentials);
ASSERT_TRUE(conn_result);
auto conn = conn_result.value();

// Execute a DO block that raises a notice
auto result = conn->execute(R"(
DO $$
BEGIN
RAISE NOTICE 'Hello from PL/pgSQL';
END
$$;
)");
ASSERT_TRUE(result) << result.error().what();

// Verify notice was captured
std::lock_guard<std::mutex> lock(mtx);
ASSERT_EQ(captured_notices.size(), 1);
EXPECT_TRUE(captured_notices[0].find("Hello from PL/pgSQL") !=
std::string::npos);
}

TEST(postgres, no_notice_handler_works) {
// Default behavior - no handler, should not crash
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();

auto result = conn->execute(R"(
DO $$
BEGIN
RAISE NOTICE 'This goes to stderr by default';
END
$$;
)");
ASSERT_TRUE(result);
}

TEST(postgres, notice_processor_multiple_notices) {
std::vector<std::string> captured_notices;
std::mutex mtx;

const auto credentials = sqlgen::postgres::Credentials{
.user = "postgres",
.password = "password",
.host = "localhost",
.dbname = "postgres",
.notice_handler = [&](const char* msg) {
std::lock_guard<std::mutex> lock(mtx);
captured_notices.push_back(msg);
}};

auto conn_result = sqlgen::postgres::connect(credentials);
ASSERT_TRUE(conn_result);
auto conn = conn_result.value();

// Execute a DO block that raises multiple notices
auto result = conn->execute(R"(
DO $$
BEGIN
RAISE NOTICE 'First notice';
RAISE NOTICE 'Second notice';
RAISE NOTICE 'Third notice';
END
$$;
)");
ASSERT_TRUE(result) << result.error().what();

// Verify all notices were captured
std::lock_guard<std::mutex> lock(mtx);
ASSERT_EQ(captured_notices.size(), 3);
EXPECT_TRUE(captured_notices[0].find("First notice") != std::string::npos);
EXPECT_TRUE(captured_notices[1].find("Second notice") != std::string::npos);
EXPECT_TRUE(captured_notices[2].find("Third notice") != std::string::npos);
}

} // namespace test_notice_processor

#endif
Loading