@@ -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