Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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. 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
20 changes: 19 additions & 1 deletion doc_classes/SQLite.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@
[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. 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 +125,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
154 changes: 112 additions & 42 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,6 +220,48 @@ bool SQLite::query(const String &p_query) {
return query_with_bindings(p_query, Array());
}

static bool 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::FLOAT:
sqlite3_bind_double(stmt, i + 1, binding_value);
break;

case Variant::STRING:
{
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;

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!");
sqlite3_finalize(stmt);
return false;
}
return true;
}

bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
const char *zErrMsg, *sql, *pzTail;
int rc;
Expand Down Expand Up @@ -254,48 +297,30 @@ bool SQLite::query_with_bindings(const String &p_query, Array param_bindings) {
/* 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;

case Variant::FLOAT:
sqlite3_bind_double(stmt, i + 1, binding_value);
break;
if (!bind_parameter(binding_value, stmt, i)) {
return false;
}
}
param_bindings = param_bindings.slice(parameter_count, param_bindings.size());

case Variant::STRING:
{
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;
if (!execute_statement(stmt, rc, zErrMsg)) {
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;
}
/* 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_bindings(sTail, param_bindings);
}

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;
}
if (!param_bindings.is_empty()) {
WARN_PRINT("GDSQLite Warning: Provided number of bindings exceeded the required number in statement! (" + String(std::to_string(param_bindings.size()).c_str()) + " unused parameter(s))");
}
param_bindings = param_bindings.slice(parameter_count, param_bindings.size());

return true;
}

bool SQLite::execute_statement(sqlite3_stmt *stmt, int &rc, const char *&zErrMsg) {
if (verbosity_level > VerbosityLevel::NORMAL) {
char *expanded_sql = sqlite3_expanded_sql(stmt);
UtilityFunctions::print(String::utf8(expanded_sql));
Expand Down Expand Up @@ -359,20 +384,65 @@ 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_named_bindings(const String &p_query, Dictionary param_bindings) {
const char *zErrMsg, *sql, *pzTail;
int rc;

if (verbosity_level > VerbosityLevel::NORMAL) {
UtilityFunctions::print(p_query);
}
const CharString dummy_query = p_query.utf8();
sql = dummy_query.get_data();

/* Clear the previous query results */
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 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);
/* 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;
/* This has side effect of rechecking the dictionary for same name if its used more than once */
if (!param_bindings.has(non_prefixed_name)) {
ERR_PRINT("GDSQLite Error: Insufficient paramater names to satisfy bindings in statement! Missing parameter: " + String::utf8(param_name));
sqlite3_finalize(stmt);
return false;
}
Variant binding_value = param_bindings[String::utf8(non_prefixed_name)];
if (!bind_parameter(binding_value, stmt, i)) {
return false;
}
}

if (!execute_statement(stmt, rc, zErrMsg)) {
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_bindings(sTail, param_bindings);
}

if (!param_bindings.is_empty()) {
WARN_PRINT("GDSQLite Warning: Provided number of bindings exceeded the required number in statement! (" + String(std::to_string(param_bindings.size()).c_str()) + " unused parameter(s))");
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
2 changes: 2 additions & 0 deletions src/gdsqlite.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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 Expand Up @@ -125,6 +126,7 @@ class SQLite : public RefCounted {
String get_default_extension() const;

void set_query_result(const TypedArray<Dictionary> &p_query_result);
bool execute_statement(sqlite3_stmt *stmt, int &rc, const char *&zErrMsg);
TypedArray<Dictionary> get_query_result() const;

TypedArray<Dictionary> get_query_result_by_reference() const;
Expand Down