relx 0.1.0
A Modern C++23 Type-Safe SQL Query Builder
Loading...
Searching...
No Matches
connection.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "../query/core.hpp"
4#include "../results/result.hpp"
5#include "meta.hpp"
6
7#include <expected>
8#include <memory>
9#include <sstream>
10#include <string>
11#include <string_view>
12#include <tuple>
13#include <type_traits>
14#include <vector>
15
16#include <boost/pfr.hpp>
17
18namespace relx {
19namespace connection {
20
23 std::string message;
24 int error_code = 0;
25};
26
28template <typename T>
29using ConnectionResult = std::expected<T, ConnectionError>;
30
38
41 std::string host = "localhost";
42 uint16_t port = 5432;
43 std::string dbname;
44 std::string user;
45 std::string password;
46 std::string application_name;
47 int connect_timeout = 30; // seconds
48
49 // Optional parameters
50 std::string ssl_mode; // disable, require, verify-ca, verify-full
51 std::string ssl_cert;
52 std::string ssl_key;
53 std::string ssl_root_cert;
54
57 std::string to_connection_string() const {
58 std::ostringstream conn_str;
59
60 if (!host.empty()) {
61 conn_str << "host=" << host << " ";
62 }
63 conn_str << "port=" << port << " ";
64 if (!dbname.empty()) {
65 conn_str << "dbname=" << dbname << " ";
66 }
67 if (!user.empty()) {
68 conn_str << "user=" << user << " ";
69 }
70 if (!password.empty()) {
71 conn_str << "password=" << password << " ";
72 }
73 if (!application_name.empty()) {
74 conn_str << "application_name=" << application_name << " ";
75 }
76 conn_str << "connect_timeout=" << connect_timeout << " ";
77
78 // Optional SSL parameters
79 if (!ssl_mode.empty()) {
80 conn_str << "sslmode=" << ssl_mode << " ";
81 }
82 if (!ssl_cert.empty()) {
83 conn_str << "sslcert=" << ssl_cert << " ";
84 }
85 if (!ssl_key.empty()) {
86 conn_str << "sslkey=" << ssl_key << " ";
87 }
88 if (!ssl_root_cert.empty()) {
89 conn_str << "sslrootcert=" << ssl_root_cert << " ";
90 }
91
92 std::string result = conn_str.str();
93 if (!result.empty() && result.back() == ' ') {
94 result.pop_back(); // Remove trailing space
95 }
96
97 return result;
98 }
99};
100
103public:
105 virtual ~Connection() = default;
106
109 [[nodiscard]] virtual ConnectionResult<void> connect() = 0;
110
113 [[nodiscard]] virtual ConnectionResult<void> disconnect() = 0;
114
119 [[nodiscard]]
121 const std::string& sql, const std::vector<std::string>& params = {}) = 0;
122
126 template <query::SqlExpr Query>
127 [[nodiscard]]
129 std::string sql = query.to_sql();
130 std::vector<std::string> params = query.bind_params();
131 return execute_raw(sql, params);
132 }
133
141 template <typename T, query::SqlExpr Query>
142 [[nodiscard]]
143 ConnectionResult<T> execute(const Query& query) {
144 auto result = execute(query);
145 if (!result) {
146 return std::unexpected(result.error());
147 }
148
149 const auto& result_set = *result;
150 if (result_set.empty()) {
151 return std::unexpected(ConnectionError{.message = "No results found"});
152 }
153
154 // Create an instance of T
155 T obj{};
156
157 // Get the first row of results
158 const auto& row = result_set.at(0);
159
160 // Use Boost.PFR to get the tuple type that matches our struct
161 auto structure_tie = boost::pfr::structure_tie(obj);
162
163 // Make sure the number of columns matches the number of fields in the struct
164 // TODO What if struct has less fields than columns? Why is it working?
165 if (result_set.column_count() != boost::pfr::tuple_size_v<std::remove_cvref_t<T>>) {
166 std::stringstream ss;
167 for (const auto& param : query.bind_params()) {
168 ss << param << ", ";
169 }
170 return std::unexpected(ConnectionError{
171 .message = "Column count does not match struct field count, " +
172 std::to_string(result_set.column_count()) +
173 " != " + std::to_string(boost::pfr::tuple_size_v<std::remove_cvref_t<T>>) +
174 " for struct " + typeid(T).name() + " and query " + query.to_sql() +
175 " with params " + ss.str(),
176 .error_code = -1});
177 }
178
179 // Convert each value in the result row to the appropriate type in the struct
180 try {
181 // Create a vector of values from the row
182 std::vector<std::string> values;
183 for (size_t i = 0; i < result_set.column_count(); ++i) {
184 auto cell_result = row.get_cell(i);
185 if (!cell_result) {
186 return std::unexpected(
187 ConnectionError{.message = "Failed to get cell value: " + cell_result.error().message,
188 .error_code = -1});
189 }
190 values.push_back((*cell_result)->raw_value());
191 }
192
193 // Apply tuple assignment from the row values to the struct fields
194 relx::connection::map_row_to_tuple(structure_tie, values);
195 } catch (const std::exception& e) {
196 return std::unexpected(
197 ConnectionError{.message = std::string("Failed to convert result to struct: ") + e.what(),
198 .error_code = -1});
199 }
200
201 return obj;
202 }
203
209 template <typename T, query::SqlExpr Query>
210 [[nodiscard]]
212 auto result = execute(query);
213 if (!result) {
214 return std::unexpected(result.error());
215 }
216
217 const auto& result_set = *result;
218 std::vector<T> objects;
219 objects.reserve(result_set.size());
220
221 // Check if we have at least one row to determine column count
222 if (result_set.empty()) {
223 return objects; // Return empty vector
224 }
225
226 // Make sure the number of columns matches the number of fields in the struct
227 // TODO
228 if (result_set.column_count() != boost::pfr::tuple_size_v<std::remove_cvref_t<T>>) {
229 std::stringstream ss;
230 for (const auto& param : query.bind_params()) {
231 ss << param << ", ";
232 }
233 return std::unexpected(ConnectionError{
234 .message = "Column count does not match struct field count, " +
235 std::to_string(result_set.column_count()) +
236 " != " + std::to_string(boost::pfr::tuple_size_v<std::remove_cvref_t<T>>) +
237 " for struct " + typeid(T).name() + " and query " + query.to_sql() +
238 " with params " + ss.str(),
239 .error_code = -1});
240 }
241
242 // Process each row
243 for (size_t row_idx = 0; row_idx < result_set.size(); ++row_idx) {
244 const auto& row = result_set.at(row_idx);
245 T obj{};
246 auto structure_tie = boost::pfr::structure_tie(obj);
247
248 try {
249 // Create a vector of values from the row
250 std::vector<std::string> values;
251 for (size_t i = 0; i < result_set.column_count(); ++i) {
252 auto cell_result = row.get_cell(i);
253 if (!cell_result) {
254 return std::unexpected(ConnectionError{.message = "Failed to get cell value: " +
255 cell_result.error().message,
256 .error_code = -1});
257 }
258 values.push_back((*cell_result)->raw_value());
259 }
260
261 relx::connection::map_row_to_tuple(structure_tie, values);
262 objects.push_back(std::move(obj));
263 } catch (const std::exception& e) {
264 return std::unexpected(ConnectionError{
265 .message = std::string("Failed to convert result to struct: ") + e.what(),
266 .error_code = -1});
267 }
268 }
269
270 return objects;
271 }
272
275 virtual bool is_connected() const = 0;
276
280 [[nodiscard]]
282 IsolationLevel isolation_level = IsolationLevel::ReadCommitted) = 0;
283
286 [[nodiscard]]
288
291 [[nodiscard]]
293
296 virtual bool in_transaction() const = 0;
297
298private:
299};
300
301} // namespace connection
302
303// Convenient imports from the connection namespace
308} // namespace relx
Abstract base class for database connections.
virtual ConnectionResult< void > begin_transaction(IsolationLevel isolation_level=IsolationLevel::ReadCommitted)=0
Begin a new transaction.
virtual ConnectionResult< void > connect()=0
Connect to the database.
virtual bool is_connected() const =0
Check if the connection is open.
virtual bool in_transaction() const =0
Check if a transaction is currently active.
virtual ConnectionResult< void > commit_transaction()=0
Commit the current transaction.
ConnectionResult< T > execute(const Query &query)
Execute a query and map results to a user-defined type using Boost.PFR.
ConnectionResult< result::ResultSet > execute(const Query &query)
Execute a query expression.
virtual ConnectionResult< result::ResultSet > execute_raw(const std::string &sql, const std::vector< std::string > &params={})=0
Execute a raw SQL query with parameters.
virtual ConnectionResult< void > disconnect()=0
Disconnect from the database.
ConnectionResult< std::vector< T > > execute_many(const Query &query)
Execute a query and map results to a vector of user-defined types.
virtual ~Connection()=default
Virtual destructor.
virtual ConnectionResult< void > rollback_transaction()=0
Rollback the current transaction.
void map_row_to_tuple(Tuple &tuple, const std::vector< std::string > &row)
Helper function to map a result row to a tuple (and thus to a struct)
Definition meta.hpp:61
IsolationLevel
Transaction isolation levels.
@ ReadUncommitted
Allows dirty reads.
@ RepeatableRead
Prevents non-repeatable reads.
@ Serializable
Highest isolation level, prevents phantom reads.
@ ReadCommitted
Prevents dirty reads.
std::expected< T, ConnectionError > ConnectionResult
Type alias for result of connection operations.
relx database connection
Error type for database connection operations.
Basic parameters for a PostgreSQL connection.
std::string to_connection_string() const
Convert parameters to a PostgreSQL connection string.