Skip to content

Introduced prepared statement APIs#238

Open
Benjamin-Dobell wants to merge 1 commit into
2shady4u:masterfrom
BreakaClub:prepared-statements
Open

Introduced prepared statement APIs#238
Benjamin-Dobell wants to merge 1 commit into
2shady4u:masterfrom
BreakaClub:prepared-statements

Conversation

@Benjamin-Dobell

@Benjamin-Dobell Benjamin-Dobell commented Mar 16, 2026

Copy link
Copy Markdown

Support for prepared statements.

API

New API exported SQLiteStatement class.

# Prepare a statement
var statement = db.prepare("SELECT id, name FROM company WHERE age >= :min_age ORDER BY id;")

# Bind SQL Parameters
statement.bind_named({"min_age": 30})

# Execute the statement
var rows = statement.fetch_all()

# Reuse the prepared statement
statement.reset() # Resets/clears the cursor
statement.clear_bindings() # Clears SQL parameter bindings

if not statement.bind_named({"min_age": 20}):
	print("Select re-bind failed: " + statement.get_error_message())
else:
	var rows: Array = []
	var step_result: int = statement.step()
	while step_result == SQLite.SQLITE_ROW:
		rows.append(statement.get_row())
		step_result = statement.step()
	if step_result != SQLite.SQLITE_DONE:
		print("Prepared step failed (" + str(step_result) + "): " + statement.get_error_message())
	print("Prepared step rows (age >= 20): " + str(rows))

# Free the statement
statement.finalize()

Please note that you really do want to call finalize() to avoid memory leaks. That said, the connection itself also tracks the prepared statements, so if you clean up the connection it will clean-up any left over prepared statements.

Tests

Tests have been updated to include code similar to the above.

Docs

XML docs have been updated too.

AI Disclosure

LLMs were used during development. If that's an concern for this repo, do not merge.

@2shady4u

Copy link
Copy Markdown
Owner

Hello @Benjamin-Dobell,

Thank you for implementing this feature; it has been long pending :)
Also thank you for the disclaimer:

LLMs were used during development. If that's an concern for this repo, do not merge.

Using LLMs is okay, as long as we do some rigorous reviewing before merging :)
I'll need some time to review it in more more depth...

@2shady4u 2shady4u left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a surface-level review; I need to review in more depth at some point.

Comment thread src/gdsqlite.cpp Outdated
active_instances.reserve(statement_instance_ids.size());

query_result.clear();
for (uint64_t instance_id : statement_instance_ids) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to use auto

Comment thread src/gdsqlite.cpp Outdated
}

void SQLite::finalize_statements() {
for (uint64_t instance_id : statement_instance_ids) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to use auto

Comment thread src/gdsqlite_statement.cpp Outdated
}

if (stmt == nullptr) {
stmt = nullptr;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement is already nullptr? Why assign again?

@Benjamin-Dobell Benjamin-Dobell Mar 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as I'd love to blame AI. This was almost certainly me 😛 AI generated the first iteration but I made a heap of changes after the fact - mostly removing cruft like useless helper methods. So this was probably the direct body of a helper method that was (naively) inlined.


for (int i = 0; i < column_count; i++) {
const char *column_name = sqlite3_column_name(stmt, i);
column_names.append(String::utf8(column_name));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method clear the column_names first before appending?
To make sure the column_names is actually empty.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It bails out early if the array is not empty. Basically it's lazy initialization, so subsequent calls aren't expected to mutate the column names at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's worth noting that since these are prepared statements, the columns themselves aren't mutable. Once the statement is prepared, the columns included in the query are final. You don't reuse a statement for a different query, only for re-execution or rebinding SQL arguments.

Comment thread src/gdsqlite_statement.cpp Outdated
int rc = SQLITE_OK;

if (stmt != nullptr) {
rc = sqlite3_finalize(stmt);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for if statement because SQLite docs say:
Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op.

return true;
}

void SQLiteStatement::release_statement(StatementStatus p_next_status) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should be cleaned up a bit manually.

db = nullptr;
}

bool SQLiteStatement::check_valid(const char *p_method_name) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get the point of this method? It always returns false if status of the statement is not INITIALIZED so why not use is_valid instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a private method and sets the error message, where as is_valid is part of the public API designed for library consumers.

@Benjamin-Dobell

Copy link
Copy Markdown
Author

First round of feedback addressed. Feel free to take your time on a more thorough review - just actioning this quickly because I happened to be at the start of a work session and easier for me to do this now than worry about context switching later 😅

Probably worth noting, whilst I used an LLM to generate the first iteration, I have made extensive changes to what the LLM spat out - even introducing some of the mistakes the good ol' fashion way 😉

However, I must admit, it did save me a good chunk of time. This isn't exactly novel engineering work 😂 Bindings, porting code, and somewhat surprisingly, reverse engineering, are the areas I've found LLMs to really excel at. First PR I've ever submitted where AI did more than auto-complete; so figured I better flag it.

@2shady4u

2shady4u commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Hello :)
Just wanted to give small update: I am currently swamped with other stuff and won't be able to progress on this MR for a bit.
But I do intend on getting back to it since it is a very valuable MR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants