# HG changeset patch
# Node ID 69e137c1f5bfae98274ee63268821cc50880b6d2
# Parent  c324dfc5fed2785ac003846d6ce80d7949391fea
diff --git a/seagull/meson.build b/seagull/meson.build
--- a/seagull/meson.build
+++ b/seagull/meson.build
@@ -2,6 +2,7 @@
 
 SEAGULL_SOURCES = [
   'seagulldb.c',
+  'seagullmemory.c',
   'seagullmigrations.c',
   'seagullstatement.c',
   'seagulltransaction.c',
@@ -10,6 +11,7 @@
 
 SEAGULL_HEADERS = [
   'seagulldb.h',
+  'seagullmemory.h',
   'seagullmigrations.h',
   'seagullstatement.h',
   'seagulltransaction.h',
diff --git a/seagull/seagullmemory.c b/seagull/seagullmemory.c
new file mode 100644
--- /dev/null
+++ b/seagull/seagullmemory.c
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022-2025 Seagull Developers
+ *
+ * Seagull is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <sqlite3.h>
+
+#include "seagullmemory.h"
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+seagull_free(gpointer value) {
+	sqlite3_free(value);
+}
diff --git a/seagull/seagullmemory.h b/seagull/seagullmemory.h
new file mode 100644
--- /dev/null
+++ b/seagull/seagullmemory.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022-2025 Seagull Developers
+ *
+ * Seagull is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(SEAGULL_GLOBAL_HEADER_INSIDE) && !defined(SEAGULL_COMPILATION)
+# error "only <seagull.h> may be included directly"
+#endif
+
+#ifndef SEAGULL_MEMORY_H
+#define SEAGULL_MEMORY_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "seagullversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * seagull_free:
+ * @value: the value to free
+ *
+ * Frees memory allocated by SQLite3.
+ *
+ * This is a wrapper around `sqlite3_free`. It is provided for completeness and
+ * to avoid having to link to SQLite3 directly.
+ *
+ * Since: 0.3
+ */
+SEAGULL_AVAILABLE_IN_0_3
+void seagull_free(gpointer value);
+
+G_END_DECLS
+
+#endif /* SEAGULL_MEMORY_H */
diff --git a/seagull/seagullstatement.c b/seagull/seagullstatement.c
--- a/seagull/seagullstatement.c
+++ b/seagull/seagullstatement.c
@@ -241,6 +241,13 @@
 	return statement->db;
 }
 
+char *
+seagull_statement_get_expanded_sql(SeagullStatement *statement) {
+	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
+
+	return sqlite3_expanded_sql(statement->statement);
+}
+
 const char *
 seagull_statement_get_sql(SeagullStatement *statement) {
 	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
diff --git a/seagull/seagullstatement.h b/seagull/seagullstatement.h
--- a/seagull/seagullstatement.h
+++ b/seagull/seagullstatement.h
@@ -96,6 +96,21 @@
 SeagullSqlite3 *seagull_statement_get_db(SeagullStatement *statement);
 
 /**
+ * seagull_statement_get_expanded_sql:
+ *
+ * Gets the SQL statement with parameters expanded.
+ *
+ * If all parameters have not yet been bound, they will appears as their names.
+ *
+ * Returns: (transfer full): The expanded SQL statement which should be freed
+ *          by calling [func@free].
+ *
+ * Since: 0.3
+ */
+SEAGULL_AVAILABLE_IN_0_3
+char *seagull_statement_get_expanded_sql(SeagullStatement *statement);
+
+/**
  * seagull_statement_get_sql:
  *
  * Gets the SQL statement.
diff --git a/seagull/tests/test_statement.c b/seagull/tests/test_statement.c
--- a/seagull/tests/test_statement.c
+++ b/seagull/tests/test_statement.c
@@ -224,6 +224,114 @@
 	g_assert_true(result);
 }
 
+static void
+test_seagull_statement_expanded_no_parameters(void) {
+	SeagullStatement *statement = NULL;
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	char *expanded = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	statement = seagull_statement_new(db, "SELECT 1", &error);
+	g_assert_no_error(error);
+	g_assert_true(SEAGULL_IS_STATEMENT(statement));
+
+	expanded = seagull_statement_get_expanded_sql(statement);
+	g_assert_cmpstr(expanded, ==, "SELECT 1");
+	seagull_free(expanded);
+
+	g_assert_finalize_object(statement);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_statement_expanded_at_sign_not_bound(void) {
+	SeagullStatement *statement = NULL;
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	char *expanded = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	statement = seagull_statement_new(db, "SELECT @foo", &error);
+	g_assert_no_error(error);
+	g_assert_true(SEAGULL_IS_STATEMENT(statement));
+
+	expanded = seagull_statement_get_expanded_sql(statement);
+	g_assert_cmpstr(expanded, ==, "SELECT NULL");
+	seagull_free(expanded);
+
+	g_assert_finalize_object(statement);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_statement_expanded_colon_not_bound(void) {
+	SeagullStatement *statement = NULL;
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	char *expanded = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	statement = seagull_statement_new(db, "SELECT :foo", &error);
+	g_assert_no_error(error);
+	g_assert_true(SEAGULL_IS_STATEMENT(statement));
+
+	expanded = seagull_statement_get_expanded_sql(statement);
+	g_assert_cmpstr(expanded, ==, "SELECT NULL");
+	seagull_free(expanded);
+
+	g_assert_finalize_object(statement);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_statement_expanded_dollar_sign_not_bound(void) {
+	SeagullStatement *statement = NULL;
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	char *expanded = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	statement = seagull_statement_new(db, "SELECT $foo", &error);
+	g_assert_no_error(error);
+	g_assert_true(SEAGULL_IS_STATEMENT(statement));
+
+	expanded = seagull_statement_get_expanded_sql(statement);
+	g_assert_cmpstr(expanded, ==, "SELECT NULL");
+	seagull_free(expanded);
+
+	g_assert_finalize_object(statement);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -250,5 +358,14 @@
 	g_test_add_func("/seagull/statement/trailing-semi-colon",
 	                test_seagull_statement_trailing_semi_colon);
 
+	g_test_add_func("/seagull/statement/expanded/no-parameters",
+	                test_seagull_statement_expanded_no_parameters);
+	g_test_add_func("/seagull/statement/expanded/at-sign-not-bound",
+	                test_seagull_statement_expanded_at_sign_not_bound);
+	g_test_add_func("/seagull/statement/expanded/colon-not-bound",
+	                test_seagull_statement_expanded_colon_not_bound);
+	g_test_add_func("/seagull/statement/expanded/dollar-sign-not-bound",
+	                test_seagull_statement_expanded_dollar_sign_not_bound);
+
 	return g_test_run();
 }
