diff --git a/README.md b/README.md index 29365f2..c02154f 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Additionally, a video tutorial by [Mitch McCollum (finepointcgi)](https://github - Boolean success = **query_with_bindings(** String query_string, Array param_bindings **)** - 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). + 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). **Example usage**: @@ -130,6 +130,27 @@ Additionally, a video tutorial by [Mitch McCollum (finepointcgi)](https://github ***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).* +- Boolean success = **query_with_named_bindings(** String query_string, Dictionary param_bindings **)** + + 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). + + **Example usage**: + + ```gdscript + var column_name : String = "name"; + var query_string : String = "SELECT %s FROM company WHERE age < :age;" % [column_name] + var param_bindings : Dictionary = { "age": 24 } + var success = db.query_with_named_bindings(query_string, param_bindings) + # Executes following query: + # SELECT name FROM company WHERE age < 24; + ``` + + 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. + + 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. + + ***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).* + - Boolean success = **create_table(** String table_name, Dictionary table_dictionary **)** 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. diff --git a/doc_classes/SQLite.xml b/doc_classes/SQLite.xml index 87f007c..f7b0222 100644 --- a/doc_classes/SQLite.xml +++ b/doc_classes/SQLite.xml @@ -82,6 +82,26 @@ [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] + + + + 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. + 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 [url=https://www.sqlite.org/c3ref/bind_blob.html]here[/url]. + [b]Example usage[/b]: + [codeblock] + var column_name : String = "name"; + var query_string : String = "SELECT %s FROM company WHERE age < :age;" % [column_name] + var param_bindings : Dictionary = { "age": 24 } + var success = db.query_with_named_bindings(query_string, param_bindings) + # Executes following query: + # SELECT name FROM company WHERE age < 24; + [/codeblock] + 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. + 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. + [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] + + @@ -107,7 +127,7 @@ "auto_increment": true } [/codeblock] - 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]. + 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]. diff --git a/src/gdsqlite.cpp b/src/gdsqlite.cpp index 1e3cde3..b2d067b 100644 --- a/src/gdsqlite.cpp +++ b/src/gdsqlite.cpp @@ -8,6 +8,7 @@ void SQLite::_bind_methods() { ClassDB::bind_method(D_METHOD("close_db"), &SQLite::close_db); ClassDB::bind_method(D_METHOD("query", "query_string"), &SQLite::query); ClassDB::bind_method(D_METHOD("query_with_bindings", "query_string", "param_bindings"), &SQLite::query_with_bindings); + ClassDB::bind_method(D_METHOD("query_with_named_bindings", "query_string", "param_bindings"), &SQLite::query_with_named_bindings); ClassDB::bind_method(D_METHOD("create_table", "table_name", "table_data"), &SQLite::create_table); ClassDB::bind_method(D_METHOD("drop_table", "table_name"), &SQLite::drop_table); @@ -219,84 +220,70 @@ bool SQLite::query(const String &p_query) { return query_with_bindings(p_query, Array()); } -bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) { - const char *zErrMsg, *sql, *pzTail; - int rc; +bool SQLite::prepare_statement(const CharString &p_query, sqlite3_stmt **out_stmt, const char** pzTail) { + if (verbosity_level > VerbosityLevel::NORMAL) { + UtilityFunctions::print(p_query.get_data()); + } - if (verbosity_level > VerbosityLevel::NORMAL) { - UtilityFunctions::print(p_query); - } - const CharString dummy_query = p_query.utf8(); - sql = dummy_query.get_data(); + const char *sql = p_query.get_data(); - /* Clear the previous query results */ - query_result.clear(); + query_result.clear(); - sqlite3_stmt *stmt; - /* Prepare an SQL statement */ - rc = sqlite3_prepare_v2(db, sql, -1, &stmt, &pzTail); - zErrMsg = sqlite3_errmsg(db); - error_message = String::utf8(zErrMsg); - if (rc != SQLITE_OK) { - ERR_PRINT(" --> SQL error: " + error_message); - sqlite3_finalize(stmt); - return false; - } + int rc = sqlite3_prepare_v2(db, sql, -1, out_stmt, pzTail); + const char *zErrMsg = sqlite3_errmsg(db); + error_message = String::utf8(zErrMsg); - /* Check if the param_bindings size exceeds the required parameter count */ - int parameter_count = sqlite3_bind_parameter_count(stmt); - if (param_bindings.size() < parameter_count) { - ERR_PRINT("GDSQLite Error: Insufficient number of parameters to satisfy required number of bindings in statement!"); - sqlite3_finalize(stmt); - return false; - } - - /* Bind any given parameters to the prepared statement */ - for (int i = 0; i < parameter_count; i++) { - Variant binding_value = param_bindings.get(i); - switch (binding_value.get_type()) { - case Variant::NIL: - sqlite3_bind_null(stmt, i + 1); - break; - - case Variant::BOOL: - case Variant::INT: - sqlite3_bind_int64(stmt, i + 1, int64_t(binding_value)); - break; + if (rc != SQLITE_OK) { + ERR_PRINT(" --> SQL error: " + error_message); + sqlite3_finalize(*out_stmt); + return false; + } - case Variant::FLOAT: - sqlite3_bind_double(stmt, i + 1, binding_value); - break; + return true; +} - case Variant::STRING: - case Variant::STRING_NAME: - { - const CharString dummy_binding = (binding_value.operator String()).utf8(); - const char *binding = dummy_binding.get_data(); - sqlite3_bind_text(stmt, i + 1, binding, -1, SQLITE_TRANSIENT); - } - break; +bool SQLite::bind_parameter(Variant binding_value, sqlite3_stmt *stmt, int i) { + switch (binding_value.get_type()) { + case Variant::NIL: + sqlite3_bind_null(stmt, i + 1); + break; + case Variant::BOOL: + case Variant::INT: + sqlite3_bind_int64(stmt, i + 1, int64_t(binding_value)); + break; - case Variant::PACKED_BYTE_ARRAY: { - PackedByteArray binding = ((const PackedByteArray &)binding_value); - /* Calling .ptr() on an empty PackedByteArray returns an error */ - if (binding.size() == 0) { - sqlite3_bind_null(stmt, i + 1); - /* Identical to: `sqlite3_bind_blob64(stmt, i + 1, nullptr, 0, SQLITE_TRANSIENT);`*/ - } else { - sqlite3_bind_blob64(stmt, i + 1, binding.ptr(), binding.size(), SQLITE_TRANSIENT); - } - break; + case Variant::FLOAT: + sqlite3_bind_double(stmt, i + 1, binding_value); + break; + case Variant::STRING: + case Variant::STRING_NAME: + { + const CharString dummy_binding = (binding_value.operator String()).utf8(); + const char *binding = dummy_binding.get_data(); + sqlite3_bind_text(stmt, i + 1, binding, -1, SQLITE_TRANSIENT); } + break; - default: - ERR_PRINT("GDSQLite Error: Binding a parameter of type " + String(std::to_string(binding_value.get_type()).c_str()) + " (TYPE_*) is not supported!"); - sqlite3_finalize(stmt); - return false; + case Variant::PACKED_BYTE_ARRAY: { + PackedByteArray binding = ((const PackedByteArray &)binding_value); + /* Calling .ptr() on an empty PackedByteArray returns an error */ + if (binding.size() == 0) { + sqlite3_bind_null(stmt, i + 1); + /* Identical to: `sqlite3_bind_blob64(stmt, i + 1, nullptr, 0, SQLITE_TRANSIENT);`*/ + } else { + sqlite3_bind_blob64(stmt, i + 1, binding.ptr(), binding.size(), SQLITE_TRANSIENT); + } + break; } + + default: + ERR_PRINT("GDSQLite Error: Binding a parameter of type " + String(std::to_string(binding_value.get_type()).c_str()) + " (TYPE_*) is not supported!"); + return false; } - param_bindings = param_bindings.slice(parameter_count, param_bindings.size()); + return true; +} +bool SQLite::execute_statement(sqlite3_stmt *stmt) { if (verbosity_level > VerbosityLevel::NORMAL) { char *expanded_sql = sqlite3_expanded_sql(stmt); UtilityFunctions::print(String::utf8(expanded_sql)); @@ -359,8 +346,8 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) { /* Clean up and delete the resources used by the prepared statement */ sqlite3_finalize(stmt); - rc = sqlite3_errcode(db); - zErrMsg = sqlite3_errmsg(db); + int rc = sqlite3_errcode(db); + const char *zErrMsg = sqlite3_errmsg(db); error_message = String::utf8(zErrMsg); if (rc != SQLITE_OK) { ERR_PRINT(" --> SQL error: " + error_message); @@ -368,6 +355,39 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) { } else if (verbosity_level > VerbosityLevel::NORMAL) { UtilityFunctions::print(" --> Query succeeded"); } + return true; +} + +bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) { + const char *pzTail; + sqlite3_stmt *stmt; + + CharString char_query = p_query.utf8(); + if (!prepare_statement(char_query, &stmt, &pzTail)) { + return false; + } + + /* Check if the param_bindings size exceeds the required parameter count */ + int parameter_count = sqlite3_bind_parameter_count(stmt); + if (param_bindings.size() < parameter_count) { + ERR_PRINT("GDSQLite Error: Insufficient number of parameters to satisfy required number of bindings in statement!"); + sqlite3_finalize(stmt); + return false; + } + + /* Bind any given parameters to the prepared statement */ + for (int i = 0; i < parameter_count; i++) { + Variant binding_value = param_bindings.get(i); + if (!bind_parameter(binding_value, stmt, i)) { + sqlite3_finalize(stmt); + return false; + } + } + param_bindings = param_bindings.slice(parameter_count, param_bindings.size()); + + if (!execute_statement(stmt)) { + return false; + } /* Figure out if there's a subsequent statement which needs execution */ String sTail = String::utf8(pzTail).strip_edges(); @@ -382,6 +402,60 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) { return true; } +bool SQLite::query_with_named_bindings(const String &p_query, Dictionary param_bindings) { + const char *pzTail; + sqlite3_stmt *stmt; + + CharString char_query = p_query.utf8(); + if (!prepare_statement(char_query, &stmt, &pzTail)) { + return false; + } + + int parameter_count = sqlite3_bind_parameter_count(stmt); + /* Bind any given parameters to the prepared statement */ + for (int i = 0; i < parameter_count; i++) { + const char *param_name = sqlite3_bind_parameter_name(stmt, i + 1); + if (nullptr == param_name) { + ERR_PRINT(vformat( + "GDSQLite Error: Parameter index %d is most likely nameless and can't assign named parameter!", + i + 1 + )); + sqlite3_finalize(stmt); + return false; + } + /* Sqlite will return the parameter name prefixed for example ?, :, $, @ but we want user to just pass in the name itself */ + const char *non_prefixed_name = param_name + 1; + Variant binding_value; + /* This has side effect of rechecking the dictionary for same name if its used more than once */ + if (param_bindings.has(non_prefixed_name)) { + binding_value = param_bindings[non_prefixed_name]; + } else { + ERR_PRINT(vformat( + "GDSQLite Error: Insufficient parameter names to satisfy bindings in statement! Missing parameter: %s", + String::utf8(non_prefixed_name) + )); + sqlite3_finalize(stmt); + return false; + } + if (!bind_parameter(binding_value, stmt, i)) { + sqlite3_finalize(stmt); + return false; + } + } + + if (!execute_statement(stmt)) { + return false; + } + + /* Figure out if there's a subsequent statement which needs execution */ + String sTail = String::utf8(pzTail).strip_edges(); + if (!sTail.is_empty()) { + return query_with_named_bindings(sTail, param_bindings); + } + + return true; +} + bool SQLite::create_table(const String &p_name, const Dictionary &p_table_dict) { if (!validate_table_dict(p_table_dict)) { return false; diff --git a/src/gdsqlite.hpp b/src/gdsqlite.hpp index 4743684..1a2e7d2 100644 --- a/src/gdsqlite.hpp +++ b/src/gdsqlite.hpp @@ -40,6 +40,9 @@ class SQLite : public RefCounted { bool validate_table_dict(const Dictionary &p_table_dict); int backup_database(sqlite3 *source_db, sqlite3 *destination_db); void remove_shadow_tables(Array &p_array); + bool prepare_statement(const CharString &p_query, sqlite3_stmt **out_stmt, const char** pzTail); + bool bind_parameter(Variant binding_value, sqlite3_stmt *stmt, int i); + bool execute_statement(sqlite3_stmt *stmt); String normalize_path(const String p_path, const bool read_only) const; @@ -74,6 +77,7 @@ class SQLite : public RefCounted { bool close_db(); bool query(const String &p_query); bool query_with_bindings(const String &p_query, Array param_bindings); + bool query_with_named_bindings(const String &p_query, Dictionary param_bindings); bool create_table(const String &p_name, const Dictionary &p_table_dict); bool drop_table(const String &p_name);