Skip to content

Commit 6c664b3

Browse files
authored
Add functionality to query with named bindings (#225)
1 parent 7c25edb commit 6c664b3

File tree

4 files changed

+189
-70
lines changed

4 files changed

+189
-70
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Additionally, a video tutorial by [Mitch McCollum (finepointcgi)](https://github
113113

114114
- Boolean success = **query_with_bindings(** String query_string, Array param_bindings **)**
115115

116-
Binds the parameters contained in the `param_bindings`-variable to the query. Using this function stops any possible attempts at SQL data injection as the parameters are sanitized. More information regarding parameter bindings can be found [here](https://www.sqlite.org/c3ref/bind_blob.html).
116+
Binds the parameters using nameless variables contained in the `param_bindings`-variable to the query. Using this function stops any possible attempts at SQL data injection as the parameters are sanitized. More information regarding parameter bindings can be found [here](https://www.sqlite.org/c3ref/bind_blob.html).
117117

118118
**Example usage**:
119119

@@ -130,6 +130,27 @@ Additionally, a video tutorial by [Mitch McCollum (finepointcgi)](https://github
130130
131131
***NOTE**: Binding column names is not possible due to SQLite restrictions. If dynamic column names are required, insert the column name directly into the `query_string`-variable itself (see https://github.com/2shady4u/godot-sqlite/issues/41).*
132132
133+
- Boolean success = **query_with_named_bindings(** String query_string, Dictionary param_bindings **)**
134+
135+
Binds the parameters using named variables contained in the `param_bindings`-variable to the query. This will only work with String or StringName keys in the dictionary. If the named parameter is not found in the dictionary the query will fail. Using this function stops any possible attempts at SQL data injection as the parameters are sanitized. More information regarding parameter bindings can be found [here](https://www.sqlite.org/c3ref/bind_blob.html).
136+
137+
**Example usage**:
138+
139+
```gdscript
140+
var column_name : String = "name";
141+
var query_string : String = "SELECT %s FROM company WHERE age < :age;" % [column_name]
142+
var param_bindings : Dictionary = { "age": 24 }
143+
var success = db.query_with_named_bindings(query_string, param_bindings)
144+
# Executes following query:
145+
# SELECT name FROM company WHERE age < 24;
146+
```
147+
148+
This will support the use of `:`, `@`, `$`, `?` as prefixes for the names. These are all treated the same ?age, :age, $age, @age. When passing in the dictionary only provide the word 'age' with no prefix.
149+
150+
Using bindings is optional, except for PackedByteArray (= raw binary data) which has to binded to allow the insertion and selection of BLOB data in the database.
151+
152+
***NOTE**: Binding column names is not possible due to SQLite restrictions. If dynamic column names are required, insert the column name directly into the `query_string`-variable itself (see https://github.com/2shady4u/godot-sqlite/issues/41).*
153+
133154
- Boolean success = **create_table(** String table_name, Dictionary table_dictionary **)**
134155
135156
Each key/value pair of the `table_dictionary`-variable defines a column of the table. Each key defines the name of a column in the database, while the value is a dictionary that contains further column specifications.

doc_classes/SQLite.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@
8282
[i][b]NOTE:[/b] Binding column names is not possible due to SQLite restrictions. If dynamic column names are required, insert the column name directly into the [code]query_string[/code]-variable itself (see [url=https://github.com/2shady4u/godot-sqlite/issues/41]https://github.com/2shady4u/godot-sqlite/issues/41[/url]).[/i]
8383
</description>
8484
</method>
85+
<method name="query_with_named_bindings">
86+
<return type="bool" />
87+
<description>
88+
Binds the parameters contained in the [code]param_bindings[/code]-variable to the query. This will only work with String or StringName keys in the dictionary.
89+
If the named parameter is not found in the dictionary the query will fail.
90+
Using this function stops any possible attempts at SQL data injection as the parameters are sanitized. More information regarding parameter bindings can be found [url=https://www.sqlite.org/c3ref/bind_blob.html]here[/url].
91+
[b]Example usage[/b]:
92+
[codeblock]
93+
var column_name : String = "name";
94+
var query_string : String = "SELECT %s FROM company WHERE age &lt; :age;" % [column_name]
95+
var param_bindings : Dictionary = { "age": 24 }
96+
var success = db.query_with_named_bindings(query_string, param_bindings)
97+
# Executes following query:
98+
# SELECT name FROM company WHERE age &lt; 24;
99+
[/codeblock]
100+
This will support the use of [code]:[/code], [code]@[/code], [code]$[/code], [code]?[/code] as prefixes for the names. These are all treated the same [code]?age[/code], [code]:age[/code], [code]$age[/code], [code]@age[/code]. When passing in the dictionary only provide the word [code]age[/code] with no prefix.
101+
Using bindings is optional, except for PackedByteArray (= raw binary data) which has to binded to allow the insertion and selection of BLOB data in the database.
102+
[i][b]NOTE:[/b] Binding column names is not possible due to SQLite restrictions. If dynamic column names are required, insert the column name directly into the [code]query_string[/code]-variable itself (see [url=https://github.com/2shady4u/godot-sqlite/issues/41]https://github.com/2shady4u/godot-sqlite/issues/41[/url]).[/i]
103+
</description>
104+
</method>
85105
<method name="create_table">
86106
<return type="bool" />
87107
<description>
@@ -107,7 +127,7 @@
107127
"auto_increment": true
108128
}
109129
[/codeblock]
110-
For more concrete usage examples see the [code]database.gd[/code]-file as found [url=https://github.com/2shady4u/godot-sqlite/blob/master/demo/database.gd]here[url].
130+
For more concrete usage examples see the [code]database.gd[/code]-file as found [url=https://github.com/2shady4u/godot-sqlite/blob/master/demo/database.gd]here[/url].
111131
</description>
112132
</method>
113133
<method name="drop_table">

src/gdsqlite.cpp

Lines changed: 142 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ void SQLite::_bind_methods() {
88
ClassDB::bind_method(D_METHOD("close_db"), &SQLite::close_db);
99
ClassDB::bind_method(D_METHOD("query", "query_string"), &SQLite::query);
1010
ClassDB::bind_method(D_METHOD("query_with_bindings", "query_string", "param_bindings"), &SQLite::query_with_bindings);
11+
ClassDB::bind_method(D_METHOD("query_with_named_bindings", "query_string", "param_bindings"), &SQLite::query_with_named_bindings);
1112

1213
ClassDB::bind_method(D_METHOD("create_table", "table_name", "table_data"), &SQLite::create_table);
1314
ClassDB::bind_method(D_METHOD("drop_table", "table_name"), &SQLite::drop_table);
@@ -219,84 +220,70 @@ bool SQLite::query(const String &p_query) {
219220
return query_with_bindings(p_query, Array());
220221
}
221222

222-
bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
223-
const char *zErrMsg, *sql, *pzTail;
224-
int rc;
223+
bool SQLite::prepare_statement(const CharString &p_query, sqlite3_stmt **out_stmt, const char** pzTail) {
224+
if (verbosity_level > VerbosityLevel::NORMAL) {
225+
UtilityFunctions::print(p_query.get_data());
226+
}
225227

226-
if (verbosity_level > VerbosityLevel::NORMAL) {
227-
UtilityFunctions::print(p_query);
228-
}
229-
const CharString dummy_query = p_query.utf8();
230-
sql = dummy_query.get_data();
228+
const char *sql = p_query.get_data();
231229

232-
/* Clear the previous query results */
233-
query_result.clear();
230+
query_result.clear();
234231

235-
sqlite3_stmt *stmt;
236-
/* Prepare an SQL statement */
237-
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, &pzTail);
238-
zErrMsg = sqlite3_errmsg(db);
239-
error_message = String::utf8(zErrMsg);
240-
if (rc != SQLITE_OK) {
241-
ERR_PRINT(" --> SQL error: " + error_message);
242-
sqlite3_finalize(stmt);
243-
return false;
244-
}
232+
int rc = sqlite3_prepare_v2(db, sql, -1, out_stmt, pzTail);
233+
const char *zErrMsg = sqlite3_errmsg(db);
234+
error_message = String::utf8(zErrMsg);
245235

246-
/* Check if the param_bindings size exceeds the required parameter count */
247-
int parameter_count = sqlite3_bind_parameter_count(stmt);
248-
if (param_bindings.size() < parameter_count) {
249-
ERR_PRINT("GDSQLite Error: Insufficient number of parameters to satisfy required number of bindings in statement!");
250-
sqlite3_finalize(stmt);
251-
return false;
252-
}
253-
254-
/* Bind any given parameters to the prepared statement */
255-
for (int i = 0; i < parameter_count; i++) {
256-
Variant binding_value = param_bindings.get(i);
257-
switch (binding_value.get_type()) {
258-
case Variant::NIL:
259-
sqlite3_bind_null(stmt, i + 1);
260-
break;
261-
262-
case Variant::BOOL:
263-
case Variant::INT:
264-
sqlite3_bind_int64(stmt, i + 1, int64_t(binding_value));
265-
break;
236+
if (rc != SQLITE_OK) {
237+
ERR_PRINT(" --> SQL error: " + error_message);
238+
sqlite3_finalize(*out_stmt);
239+
return false;
240+
}
266241

267-
case Variant::FLOAT:
268-
sqlite3_bind_double(stmt, i + 1, binding_value);
269-
break;
242+
return true;
243+
}
270244

271-
case Variant::STRING:
272-
case Variant::STRING_NAME:
273-
{
274-
const CharString dummy_binding = (binding_value.operator String()).utf8();
275-
const char *binding = dummy_binding.get_data();
276-
sqlite3_bind_text(stmt, i + 1, binding, -1, SQLITE_TRANSIENT);
277-
}
278-
break;
245+
bool SQLite::bind_parameter(Variant binding_value, sqlite3_stmt *stmt, int i) {
246+
switch (binding_value.get_type()) {
247+
case Variant::NIL:
248+
sqlite3_bind_null(stmt, i + 1);
249+
break;
250+
case Variant::BOOL:
251+
case Variant::INT:
252+
sqlite3_bind_int64(stmt, i + 1, int64_t(binding_value));
253+
break;
279254

280-
case Variant::PACKED_BYTE_ARRAY: {
281-
PackedByteArray binding = ((const PackedByteArray &)binding_value);
282-
/* Calling .ptr() on an empty PackedByteArray returns an error */
283-
if (binding.size() == 0) {
284-
sqlite3_bind_null(stmt, i + 1);
285-
/* Identical to: `sqlite3_bind_blob64(stmt, i + 1, nullptr, 0, SQLITE_TRANSIENT);`*/
286-
} else {
287-
sqlite3_bind_blob64(stmt, i + 1, binding.ptr(), binding.size(), SQLITE_TRANSIENT);
288-
}
289-
break;
255+
case Variant::FLOAT:
256+
sqlite3_bind_double(stmt, i + 1, binding_value);
257+
break;
258+
case Variant::STRING:
259+
case Variant::STRING_NAME:
260+
{
261+
const CharString dummy_binding = (binding_value.operator String()).utf8();
262+
const char *binding = dummy_binding.get_data();
263+
sqlite3_bind_text(stmt, i + 1, binding, -1, SQLITE_TRANSIENT);
290264
}
265+
break;
291266

292-
default:
293-
ERR_PRINT("GDSQLite Error: Binding a parameter of type " + String(std::to_string(binding_value.get_type()).c_str()) + " (TYPE_*) is not supported!");
294-
sqlite3_finalize(stmt);
295-
return false;
267+
case Variant::PACKED_BYTE_ARRAY: {
268+
PackedByteArray binding = ((const PackedByteArray &)binding_value);
269+
/* Calling .ptr() on an empty PackedByteArray returns an error */
270+
if (binding.size() == 0) {
271+
sqlite3_bind_null(stmt, i + 1);
272+
/* Identical to: `sqlite3_bind_blob64(stmt, i + 1, nullptr, 0, SQLITE_TRANSIENT);`*/
273+
} else {
274+
sqlite3_bind_blob64(stmt, i + 1, binding.ptr(), binding.size(), SQLITE_TRANSIENT);
275+
}
276+
break;
296277
}
278+
279+
default:
280+
ERR_PRINT("GDSQLite Error: Binding a parameter of type " + String(std::to_string(binding_value.get_type()).c_str()) + " (TYPE_*) is not supported!");
281+
return false;
297282
}
298-
param_bindings = param_bindings.slice(parameter_count, param_bindings.size());
283+
return true;
284+
}
299285

286+
bool SQLite::execute_statement(sqlite3_stmt *stmt) {
300287
if (verbosity_level > VerbosityLevel::NORMAL) {
301288
char *expanded_sql = sqlite3_expanded_sql(stmt);
302289
UtilityFunctions::print(String::utf8(expanded_sql));
@@ -359,15 +346,48 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
359346
/* Clean up and delete the resources used by the prepared statement */
360347
sqlite3_finalize(stmt);
361348

362-
rc = sqlite3_errcode(db);
363-
zErrMsg = sqlite3_errmsg(db);
349+
int rc = sqlite3_errcode(db);
350+
const char *zErrMsg = sqlite3_errmsg(db);
364351
error_message = String::utf8(zErrMsg);
365352
if (rc != SQLITE_OK) {
366353
ERR_PRINT(" --> SQL error: " + error_message);
367354
return false;
368355
} else if (verbosity_level > VerbosityLevel::NORMAL) {
369356
UtilityFunctions::print(" --> Query succeeded");
370357
}
358+
return true;
359+
}
360+
361+
bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
362+
const char *pzTail;
363+
sqlite3_stmt *stmt;
364+
365+
CharString char_query = p_query.utf8();
366+
if (!prepare_statement(char_query, &stmt, &pzTail)) {
367+
return false;
368+
}
369+
370+
/* Check if the param_bindings size exceeds the required parameter count */
371+
int parameter_count = sqlite3_bind_parameter_count(stmt);
372+
if (param_bindings.size() < parameter_count) {
373+
ERR_PRINT("GDSQLite Error: Insufficient number of parameters to satisfy required number of bindings in statement!");
374+
sqlite3_finalize(stmt);
375+
return false;
376+
}
377+
378+
/* Bind any given parameters to the prepared statement */
379+
for (int i = 0; i < parameter_count; i++) {
380+
Variant binding_value = param_bindings.get(i);
381+
if (!bind_parameter(binding_value, stmt, i)) {
382+
sqlite3_finalize(stmt);
383+
return false;
384+
}
385+
}
386+
param_bindings = param_bindings.slice(parameter_count, param_bindings.size());
387+
388+
if (!execute_statement(stmt)) {
389+
return false;
390+
}
371391

372392
/* Figure out if there's a subsequent statement which needs execution */
373393
String sTail = String::utf8(pzTail).strip_edges();
@@ -382,6 +402,60 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
382402
return true;
383403
}
384404

405+
bool SQLite::query_with_named_bindings(const String &p_query, Dictionary param_bindings) {
406+
const char *pzTail;
407+
sqlite3_stmt *stmt;
408+
409+
CharString char_query = p_query.utf8();
410+
if (!prepare_statement(char_query, &stmt, &pzTail)) {
411+
return false;
412+
}
413+
414+
int parameter_count = sqlite3_bind_parameter_count(stmt);
415+
/* Bind any given parameters to the prepared statement */
416+
for (int i = 0; i < parameter_count; i++) {
417+
const char *param_name = sqlite3_bind_parameter_name(stmt, i + 1);
418+
if (nullptr == param_name) {
419+
ERR_PRINT(vformat(
420+
"GDSQLite Error: Parameter index %d is most likely nameless and can't assign named parameter!",
421+
i + 1
422+
));
423+
sqlite3_finalize(stmt);
424+
return false;
425+
}
426+
/* Sqlite will return the parameter name prefixed for example ?, :, $, @ but we want user to just pass in the name itself */
427+
const char *non_prefixed_name = param_name + 1;
428+
Variant binding_value;
429+
/* This has side effect of rechecking the dictionary for same name if its used more than once */
430+
if (param_bindings.has(non_prefixed_name)) {
431+
binding_value = param_bindings[non_prefixed_name];
432+
} else {
433+
ERR_PRINT(vformat(
434+
"GDSQLite Error: Insufficient parameter names to satisfy bindings in statement! Missing parameter: %s",
435+
String::utf8(non_prefixed_name)
436+
));
437+
sqlite3_finalize(stmt);
438+
return false;
439+
}
440+
if (!bind_parameter(binding_value, stmt, i)) {
441+
sqlite3_finalize(stmt);
442+
return false;
443+
}
444+
}
445+
446+
if (!execute_statement(stmt)) {
447+
return false;
448+
}
449+
450+
/* Figure out if there's a subsequent statement which needs execution */
451+
String sTail = String::utf8(pzTail).strip_edges();
452+
if (!sTail.is_empty()) {
453+
return query_with_named_bindings(sTail, param_bindings);
454+
}
455+
456+
return true;
457+
}
458+
385459
bool SQLite::create_table(const String &p_name, const Dictionary &p_table_dict) {
386460
if (!validate_table_dict(p_table_dict)) {
387461
return false;

src/gdsqlite.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class SQLite : public RefCounted {
4040
bool validate_table_dict(const Dictionary &p_table_dict);
4141
int backup_database(sqlite3 *source_db, sqlite3 *destination_db);
4242
void remove_shadow_tables(Array &p_array);
43+
bool prepare_statement(const CharString &p_query, sqlite3_stmt **out_stmt, const char** pzTail);
44+
bool bind_parameter(Variant binding_value, sqlite3_stmt *stmt, int i);
45+
bool execute_statement(sqlite3_stmt *stmt);
4346

4447
String normalize_path(const String p_path, const bool read_only) const;
4548

@@ -74,6 +77,7 @@ class SQLite : public RefCounted {
7477
bool close_db();
7578
bool query(const String &p_query);
7679
bool query_with_bindings(const String &p_query, Array param_bindings);
80+
bool query_with_named_bindings(const String &p_query, Dictionary param_bindings);
7781

7882
bool create_table(const String &p_name, const Dictionary &p_table_dict);
7983
bool drop_table(const String &p_name);

0 commit comments

Comments
 (0)