Skip to content

Commit 921b04a

Browse files
authored
feat(core): add parameterized query interface to database_backend (#559)
Add select_prepared() and execute_prepared() virtual methods with default fallback implementations that expand placeholders via string interpolation. This establishes the interface for wire-level prepared statements while maintaining backward compatibility. Supports both $N (PostgreSQL-style) and ? (SQLite-style) placeholders in the fallback. Backends should override these methods with native prepared statement implementations for true SQL injection protection. Fix pre-existing broken markdown anchors in docs/README.kr.md. Part of #557
1 parent 4d889d7 commit 921b04a

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

database/core/database_backend.h

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,47 @@ class database_backend
151151
*/
152152
virtual kcenon::common::VoidResult execute_query(const std::string& query_string) = 0;
153153

154+
/**
155+
* @brief Execute a parameterized SELECT query (prepared statement)
156+
*
157+
* Parameters are bound at the wire-protocol level, providing stronger
158+
* SQL injection protection than string escaping. Backends that support
159+
* native prepared statements (PostgreSQL, SQLite) should override this.
160+
*
161+
* @param query SQL with positional placeholders ($1, $2, ... or ?, ?, ...)
162+
* @param params Parameter values to bind
163+
* @return Query results as rows, or error
164+
*
165+
* @note Default implementation falls back to string interpolation via
166+
* execute_query/select_query for backends that have not yet
167+
* implemented native prepared statement support.
168+
*/
169+
[[nodiscard]] virtual kcenon::common::Result<database_result> select_prepared(
170+
const std::string& query,
171+
const std::vector<database_value>& params)
172+
{
173+
// Default fallback: substitute params inline (less secure, but functional)
174+
auto expanded = expand_params(query, params);
175+
return select_query(expanded);
176+
}
177+
178+
/**
179+
* @brief Execute a parameterized DML/DDL query (prepared statement)
180+
*
181+
* @param query SQL with positional placeholders ($1, $2, ... or ?, ?, ...)
182+
* @param params Parameter values to bind
183+
* @return VoidResult::ok() on success, error on failure
184+
*
185+
* @see select_prepared for details on parameterized queries
186+
*/
187+
[[nodiscard]] virtual kcenon::common::VoidResult execute_prepared(
188+
const std::string& query,
189+
const std::vector<database_value>& params)
190+
{
191+
auto expanded = expand_params(query, params);
192+
return execute_query(expanded);
193+
}
194+
154195
/**
155196
* @brief Begin a transaction
156197
* @return VoidResult::ok() on success, error on failure
@@ -188,6 +229,71 @@ class database_backend
188229
* Example keys: "server_version", "connection_id", "protocol_version"
189230
*/
190231
virtual std::map<std::string, std::string> connection_info() const = 0;
232+
233+
protected:
234+
/**
235+
* @brief Expand positional parameters into a SQL string (fallback)
236+
*
237+
* Substitutes $1, $2, ... or ?, ?, ... placeholders with stringified
238+
* parameter values. Used by the default select_prepared/execute_prepared
239+
* implementations. Backends with native prepared statements should
240+
* override the virtual methods instead of relying on this.
241+
*
242+
* @warning This performs string interpolation, NOT wire-level binding.
243+
* Override select_prepared/execute_prepared for true security.
244+
*/
245+
static std::string expand_params(
246+
const std::string& query,
247+
const std::vector<database_value>& params)
248+
{
249+
std::string result = query;
250+
251+
// Replace $N placeholders (PostgreSQL-style, 1-indexed)
252+
for (size_t i = params.size(); i > 0; --i) {
253+
auto placeholder = "$" + std::to_string(i);
254+
auto pos = result.find(placeholder);
255+
if (pos != std::string::npos) {
256+
result.replace(pos, placeholder.size(), value_to_sql(params[i - 1]));
257+
}
258+
}
259+
260+
// Replace ? placeholders (SQLite-style, left-to-right)
261+
size_t param_idx = 0;
262+
auto pos = result.find('?');
263+
while (pos != std::string::npos && param_idx < params.size()) {
264+
auto val = value_to_sql(params[param_idx++]);
265+
result.replace(pos, 1, val);
266+
pos = result.find('?', pos + val.size());
267+
}
268+
269+
return result;
270+
}
271+
272+
private:
273+
static std::string value_to_sql(const database_value& val)
274+
{
275+
return std::visit([](const auto& v) -> std::string {
276+
using T = std::decay_t<decltype(v)>;
277+
if constexpr (std::is_same_v<T, std::nullptr_t>) {
278+
return "NULL";
279+
} else if constexpr (std::is_same_v<T, bool>) {
280+
return v ? "TRUE" : "FALSE";
281+
} else if constexpr (std::is_same_v<T, std::string>) {
282+
// Basic escaping — backends should override for proper security
283+
std::string escaped;
284+
escaped.reserve(v.size() + 2);
285+
escaped += '\'';
286+
for (char c : v) {
287+
if (c == '\'') escaped += "''";
288+
else escaped += c;
289+
}
290+
escaped += '\'';
291+
return escaped;
292+
} else {
293+
return std::to_string(v);
294+
}
295+
}, val);
296+
}
191297
};
192298

193299
/**

0 commit comments

Comments
 (0)