Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand All @@ -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.
Expand Down
22 changes: 21 additions & 1 deletion doc_classes/SQLite.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
</description>
</method>
<method name="query_with_named_bindings">
<return type="bool" />
<description>
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 &lt; :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 &lt; 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]
</description>
</method>
<method name="create_table">
<return type="bool" />
<description>
Expand All @@ -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].
</description>
</method>
<method name="drop_table">
Expand Down
210 changes: 142 additions & 68 deletions src/gdsqlite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -359,15 +346,48 @@ 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);
return false;
} 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();
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/gdsqlite.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down