pidgin/pidgin

Add some api for handling SQLite3 migrations

20 months ago, Gary Kramlich
36c3c3cd2402
Parents 3f96e2b2b03d
Children 6ef5bb284651
Add some api for handling SQLite3 migrations

Also port the SqliteHistoryAdapter to the new api.

Testing Done:
Ran the unit tests under valgrind and checked out code coverage which is really just missing error handling I can't test.

Bugs closed: PIDGIN-17695

Reviewed at https://reviews.imfreedom.org/r/1906/
--- a/libpurple/meson.build Mon Oct 10 00:27:32 2022 -0500
+++ b/libpurple/meson.build Mon Oct 10 00:38:48 2022 -0500
@@ -85,6 +85,7 @@
'purpleprotocolwhiteboard.c',
'purpleproxyinfo.c',
'purpleroomlistroom.c',
+ 'purplesqlite3.c',
'purplesqlitehistoryadapter.c',
'purpletags.c',
'purpleuiinfo.c',
@@ -191,6 +192,7 @@
'purpleprotocolwhiteboard.h',
'purpleproxyinfo.h',
'purpleroomlistroom.h',
+ 'purplesqlite3.h',
'purplesqlitehistoryadapter.h',
'purpletags.h',
'purpleuiinfo.h',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplesqlite3.c Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,205 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gio.h>
+
+#include "purplesqlite3.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+purple_sqlite3_run_migration(sqlite3 *db, int version, const char *migration,
+ GError **error)
+{
+ char *errmsg = NULL;
+ char *str = NULL;
+ gboolean success = TRUE;
+
+ str = g_strdup_printf("BEGIN;%s;PRAGMA user_version=%d;COMMIT;", migration,
+ version);
+
+ sqlite3_exec(db, str, NULL, NULL, &errmsg);
+ if(errmsg != NULL) {
+ g_set_error(error, PURPLE_SQLITE3_DOMAIN, 0,
+ "failed to run migration: %s", errmsg);
+
+ sqlite3_free(errmsg);
+
+ sqlite3_exec(db, "ROLLBACK", NULL, NULL, &errmsg);
+ if(errmsg != NULL) {
+ g_error("failed to rollback transaction: %s", errmsg);
+
+ sqlite3_free(errmsg);
+ }
+
+ success = FALSE;
+ }
+
+ g_free(str);
+
+ return success;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+int
+purple_sqlite3_get_schema_version(sqlite3 *db, GError **error) {
+ sqlite3_stmt *stmt = NULL;
+ int version = -1;
+
+ g_return_val_if_fail(db != NULL, -1);
+
+ sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, NULL);
+
+ if(stmt == NULL) {
+ g_set_error(error, PURPLE_SQLITE3_DOMAIN, 0,
+ "error while creating prepared statement: %s",
+ sqlite3_errmsg(db));
+
+ return -1;
+ }
+
+ if(sqlite3_step(stmt) == SQLITE_ROW) {
+ version = sqlite3_column_int(stmt, 0);
+ } else {
+ g_set_error_literal(error, PURPLE_SQLITE3_DOMAIN, 0,
+ "'PRAGMA user_version' didn't return a row");
+
+ sqlite3_finalize(stmt);
+
+ return -1;
+ }
+
+ sqlite3_finalize(stmt);
+
+ return version;
+}
+
+gboolean
+purple_sqlite3_run_migrations_from_strings(sqlite3 *db,
+ const char *migrations[],
+ GError **error)
+{
+ int current_version = 0;
+ guint n_migrations = 0;
+
+ g_return_val_if_fail(db != NULL, FALSE);
+ g_return_val_if_fail(migrations != NULL, FALSE);
+
+ /* Get the current version or bail if it failed. */
+ current_version = purple_sqlite3_get_schema_version(db, error);
+ if(current_version == -1) {
+ return FALSE;
+ }
+
+ n_migrations = g_strv_length((char **)migrations);
+ if((guint)current_version > n_migrations) {
+ g_set_error(error, PURPLE_SQLITE3_DOMAIN, 0,
+ "schema version %u is higher than known migrations %u",
+ (guint)current_version, n_migrations);
+
+ return FALSE;
+ }
+
+ for(int i = current_version; migrations[i] != NULL; i++) {
+ int version = i + 1;
+
+ if(!purple_sqlite3_run_migration(db, version, migrations[i], error)) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+purple_sqlite3_run_migrations_from_resources(sqlite3 *db, const char *path,
+ const char *migrations[],
+ GError **error)
+{
+ GError *local_error = NULL;
+ int current_version = 0;
+ guint n_migrations = 0;
+
+ g_return_val_if_fail(db != NULL, FALSE);
+ g_return_val_if_fail(path != NULL, FALSE);
+ g_return_val_if_fail(migrations != NULL, FALSE);
+
+ /* Get the current version or bail if it failed. */
+ current_version = purple_sqlite3_get_schema_version(db, error);
+ if(current_version == -1) {
+ return FALSE;
+ }
+
+ n_migrations = g_strv_length((char **)migrations);
+ if((guint)current_version > n_migrations) {
+ g_set_error(error, PURPLE_SQLITE3_DOMAIN, 0,
+ "schema version %u is higher than known migrations %u",
+ (guint)current_version, n_migrations);
+
+ return FALSE;
+ }
+
+ /* `PRAGMA user_version` starts at 0, so write our version as i + 1. We
+ * start iterating the list of migrations at the current version of the
+ * database. If the database is already up to date, then current_version
+ * will point us at the null terminator in the list of migrations, which
+ * will short circuit the for loop.
+ */
+ for(int i = current_version; migrations[i] != NULL; i++) {
+ GBytes *data = NULL;
+ char *full_path = NULL;
+ const gchar *migration = NULL;
+ int version = i + 1;
+
+ /* Get the data from the resource */
+ full_path = g_build_path("/", path, migrations[i], NULL);
+
+ data = g_resources_lookup_data(full_path, G_RESOURCE_LOOKUP_FLAGS_NONE,
+ &local_error);
+ if(data == NULL || local_error != NULL) {
+ if(local_error == NULL) {
+ local_error = g_error_new(PURPLE_SQLITE3_DOMAIN, 0,
+ "failed to load resource %s",
+ full_path);
+ }
+
+ g_propagate_error(error, local_error);
+
+ g_clear_pointer(&data, g_bytes_unref);
+ g_free(full_path);
+
+ return FALSE;
+ }
+
+ g_free(full_path);
+
+ migration = (const char *)g_bytes_get_data(data, NULL);
+ if(!purple_sqlite3_run_migration(db, version, migration, error)) {
+ g_bytes_unref(data);
+
+ return FALSE;
+ }
+
+ g_bytes_unref(data);
+ }
+
+ return TRUE;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplesqlite3.h Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,125 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_SQLITE3_H
+#define PURPLE_SQLITE3_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include <sqlite3.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PURPLE_SQLITE3_ERROR:
+ *
+ * An error domain for sqlite3 errors.
+ *
+ * Since: 3.0.0
+ */
+#define PURPLE_SQLITE3_DOMAIN (g_quark_from_static_string("sqlite3"))
+
+/**
+ * purple_sqlite3_get_schema_version:
+ * @db: The sqlite3 connection.
+ * @error: (nullable): A return address for a [type@GLib.Error].
+ *
+ * Attempts to read the result of `PRAGMA user_version` which this API uses to
+ * store the schema version.
+ *
+ * Returns: %TRUE on success, or %FALSE on error with @error set.
+ *
+ * Since: 3.0.0
+ */
+int purple_sqlite3_get_schema_version(sqlite3 *db, GError **error);
+
+/**
+ * purple_sqlite3_run_migrations_from_strings:
+ * @db: The sqlite3 connection.
+ * @migrations: (array zero-terminated=1): A list of SQL statements, each item
+ * being its own migration.
+ * @error: (nullable): A return address for a [type@GLib.Error].
+ *
+ * Runs the given migrations in the order they are given. The index of each
+ * migration plus 1 is assumed is to be the version number of the migration,
+ * which means that you can not change the order of the migrations. The
+ * reasoning for the addition of 1 is because `PRAGMA user_version` defaults to
+ * 0.
+ *
+ * This expects each string in @migrations to be a complete migration. That is,
+ * each string in the array should contain all of the SQL for that migration.
+ * For example, if you're expecting to have 2 migrations, the initial creating
+ * two tables, and then adding a column to one of the existing tables, you
+ * would have something like the following code.
+ *
+ * ```c
+ * const char *migrations[] = {
+ * // Our initial migration that creates user and session tables.
+ * "CREATE TABLE user(id INTEGER PRIMARY KEY, name TEXT);"
+ * "CREATE TABLE session(user INTEGER, token TEXT) FOREIGN KEY(user) REFERENCES user(id);",
+ * // Begin our second migration that will add a display name to the user
+ * // table. Note the ',' at the end of the previous line.
+ * "ALTER TABLE user ADD COLUMN(display_name TEXT);",
+ * NULL
+ * };
+ * ```
+ *
+ * Also, this function will run each migration in its own transaction so you
+ * don't need to worry about them. This is done to make sure that the database
+ * stays at a known version and an incomplete migration will not be saved.
+ *
+ * Returns: %TRUE on success, or %FALSE on error potentially with @error set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_sqlite3_run_migrations_from_strings(sqlite3 *db, const char *migrations[], GError **error);
+
+/**
+ * purple_sqlite3_run_migrations_from_resources:
+ * @db: The sqlite3 connection.
+ * @path: The base path in @resource to use.
+ * @migrations: (array zero-terminated=1): The list of migrations in the order
+ * to run them.
+ * @error: (nullable): A return address for a [type@GLib.Error].
+ *
+ * Runs the given migrations in the order they are given. The index of each
+ * migration plus 1 is assumed to be the version number of the migration, which
+ * means that you can not change the order of the migrations. The reasoning for
+ * the addition of 1 is because `PRAGMA user_version` defaults to 0.
+ *
+ * This will attempt to load the migrations via
+ * [func@Gio.resources_open_stream] by concatenating @path and the individual
+ * items of @migrations. Each migration will be ran in a transaction that
+ * includes updating the schema version, which is stored in
+ * `PRAGMA user_version`. This means you can't use `PRAGMA user_version` for
+ * other things.
+ *
+ * Returns: %TRUE on success, or %FALSE on error potentially with @error set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_sqlite3_run_migrations_from_resources(sqlite3 *db, const char *path, const char *migrations[], GError **error);
+
+G_END_DECLS
+
+#endif /* PURPLE_SQLITE3_H */
--- a/libpurple/purplesqlitehistoryadapter.c Mon Oct 10 00:27:32 2022 -0500
+++ b/libpurple/purplesqlitehistoryadapter.c Mon Oct 10 00:38:48 2022 -0500
@@ -18,13 +18,13 @@
#include <glib/gi18n-lib.h>
+#include <sqlite3.h>
+
#include "purplesqlitehistoryadapter.h"
#include "account.h"
#include "purpleprivate.h"
-#include "purpleresources.h"
-
-#include <sqlite3.h>
+#include "purplesqlite3.h"
struct _PurpleSqliteHistoryAdapter {
PurpleHistoryAdapter parent;
@@ -60,34 +60,14 @@
purple_sqlite_history_adapter_run_migrations(PurpleSqliteHistoryAdapter *adapter,
GError **error)
{
- GBytes *bytes = NULL;
- GResource *resource = NULL;
- gchar *error_msg = NULL;
- const gchar *script = NULL;
-
- resource = purple_get_resource();
-
- bytes = g_resource_lookup_data(resource,
- "/im/pidgin/libpurple/sqlitehistoryadapter/01-schema.sql",
- G_RESOURCE_LOOKUP_FLAGS_NONE, error);
- if(bytes == NULL) {
- return FALSE;
- }
+ const char *path = "/im/pidgin/libpurple/sqlitehistoryadapter";
+ const char *migrations[] = {
+ "01-schema.sql",
+ NULL
+ };
- script = (const gchar *)g_bytes_get_data(bytes, NULL);
- sqlite3_exec(adapter->db, script, NULL, NULL, &error_msg);
- g_bytes_unref(bytes);
-
- if(error_msg != NULL) {
- g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
- "failed to run migrations: %s", error_msg);
-
- sqlite3_free(error_msg);
-
- return FALSE;
- }
-
- return TRUE;
+ return purple_sqlite3_run_migrations_from_resources(adapter->db, path,
+ migrations, error);
}
static gchar *
--- a/libpurple/resources/sqlitehistoryadapter/01-schema.sql Mon Oct 10 00:27:32 2022 -0500
+++ b/libpurple/resources/sqlitehistoryadapter/01-schema.sql Mon Oct 10 00:38:48 2022 -0500
@@ -1,4 +1,4 @@
-CREATE TABLE IF NOT EXISTS message_log
+CREATE TABLE message_log
(
protocol TEXT NOT NULL, -- examples: slack, xmpp, irc, discord
account TEXT NOT NULL, -- example: grim@reaperworld.com@milwaukee.slack.com
--- a/libpurple/tests/meson.build Mon Oct 10 00:27:32 2022 -0500
+++ b/libpurple/tests/meson.build Mon Oct 10 00:38:48 2022 -0500
@@ -59,4 +59,6 @@
dependencies : [libpurple_dep, glib],
link_with: test_ui,
)
-test('credential_manager', e, env: testenv, is_parallel : false)
\ No newline at end of file
+test('credential_manager', e, env: testenv, is_parallel : false)
+
+subdir('sqlite3')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/initial.sql Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,2 @@
+CREATE TABLE foo(a TEXT);
+CREATE TABLE bar(b TEXT);
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/malformed.sql Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,1 @@
+CREATE TABLE foo(a TEXT;
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/meson.build Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,16 @@
+TEST_SQLITE3_SOURCES = [
+ 'test_sqlite3.c'
+]
+
+TEST_SQLITE3_RESOURCES = gnome.compile_resources('test_sqlite3_resources',
+ 'test_sqlite3.gresource.xml',
+ source_dir : '.',
+ c_name : 'test_sqlite3')
+TEST_SQLITE3_SOURCES += TEST_SQLITE3_RESOURCES
+
+test_sqlite3 = executable(
+ 'test_sqlite3',
+ TEST_SQLITE3_SOURCES,
+ dependencies : [libpurple_dep, glib, sqlite3])
+
+test('sqlite3', test_sqlite3)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/secondary.sql Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,1 @@
+CREATE TABLE baz(c TEXT);
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/test_sqlite3.c Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,476 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 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 <glib.h>
+
+#include <sqlite3.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * get schema version tests
+ *****************************************************************************/
+static void
+test_sqlite3_get_schema_version_null(void) {
+ if(g_test_subprocess()) {
+ GError *error = NULL;
+ int version = 0;
+
+ version = purple_sqlite3_get_schema_version(NULL, &error);
+ g_assert_error(error, PURPLE_SQLITE3_DOMAIN, 0);
+ g_assert_cmpint(version, ==, -1);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("*assertion*!= NULL*");
+}
+
+static void
+test_sqlite3_get_schema_version_new(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ int rc = 0;
+ int version = 0;
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 0);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+/******************************************************************************
+ * string migration tests
+ *****************************************************************************/
+static void
+test_sqlite3_string_migrations_null(void) {
+ if(g_test_subprocess()) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_strings(db, NULL, &error);
+ g_assert_no_error(error);
+ g_assert_false(res);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("*migrations != NULL*");
+}
+
+static void
+test_sqlite3_string_migrations_null_terminator(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {NULL};
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations, &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 0);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_string_migrations_multiple(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {
+ "CREATE TABLE foo(a TEXT); CREATE TABLE bar(b TEXT);",
+ "CREATE TABLE baz(c TEXT);",
+ NULL
+ };
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations, &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ /* Run the migrations again and make sure we remain at schema version 2. */
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations, &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_string_migrations_syntax_error(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {
+ "CREATE TABLE broke(a TEXT",
+ NULL
+ };
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations, &error);
+ g_assert_error(error, PURPLE_SQLITE3_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_false(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 0);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_string_migrations_older(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations1[] = {
+ "CREATE TABLE foo(a TEXT); CREATE TABLE bar(b TEXT);",
+ "CREATE TABLE baz(c TEXT);",
+ NULL
+ };
+ const char *migrations2[] = {
+ "CREATE TABLE foo(a TEXT); CREATE TABLE bar(b TEXT);",
+ NULL
+ };
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations1, &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ /* Run the older migrations now and verify we get a failure. */
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations2, &error);
+ g_assert_error(error, PURPLE_SQLITE3_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_false(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+/******************************************************************************
+ * resource migration tests
+ *****************************************************************************/
+static void
+test_sqlite3_resource_migrations_null_path(void) {
+ if(g_test_subprocess()) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, NULL, NULL,
+ &error);
+ g_assert_no_error(error);
+ g_assert_false(res);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("*path != NULL*");
+}
+
+static void
+test_sqlite3_resource_migrations_null_migrations(void) {
+ if(g_test_subprocess()) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ const char *path = "/im/libpidgin/purple/tests/sqlite3/";
+ int rc = 0;
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, NULL,
+ &error);
+ g_assert_no_error(error);
+ g_assert_false(res);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("*migrations != NULL*");
+}
+
+static void
+test_sqlite3_resource_migrations_null_terminator(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {NULL};
+ const char *path = "/im/pidgin/libpurple/tests/sqlite3/";
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations,
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 0);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_resource_migrations_multiple(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {"initial.sql", "secondary.sql", NULL};
+ const char *path = "/im/pidgin/libpurple/tests/sqlite3/";
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations,
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ /* Run the migrations again and make sure we remain at schema version 2. */
+ res = purple_sqlite3_run_migrations_from_strings(db, migrations, &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_resource_migrations_missing(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {"initial.sql", "imaginary.sql", NULL};
+ const char *path = "/im/pidgin/libpurple/tests/sqlite3/";
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations,
+ &error);
+ g_assert_error(error, G_RESOURCE_ERROR, 0);
+ g_clear_error(&error);
+ g_assert_false(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 1);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_resource_migrations_syntax_error(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations[] = {"malformed.sql", NULL};
+ const char *path = "/im/pidgin/libpurple/tests/sqlite3/";
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations,
+ &error);
+ g_assert_error(error, PURPLE_SQLITE3_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_false(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 0);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+static void
+test_sqlite3_resource_migrations_older(void) {
+ GError *error = NULL;
+ sqlite3 *db = NULL;
+ gboolean res = FALSE;
+ int rc = 0;
+ int version = -1;
+ const char *migrations1[] = {"initial.sql", "secondary.sql", NULL};
+ const char *migrations2[] = {"initial.sql", NULL};
+ const char *path = "/im/pidgin/libpurple/tests/sqlite3/";
+
+ rc = sqlite3_open(":memory:", &db);
+ g_assert_nonnull(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations1,
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ /* Run the older migrations now and verify we get a failure. */
+ res = purple_sqlite3_run_migrations_from_resources(db, path, migrations2,
+ &error);
+ g_assert_error(error, PURPLE_SQLITE3_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_false(res);
+
+ version = purple_sqlite3_get_schema_version(db, &error);
+ g_assert_no_error(error);
+ g_assert_cmpint(version, ==, 2);
+
+ rc = sqlite3_close(db);
+ g_assert_cmpint(rc, ==, SQLITE_OK);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+ g_test_init(&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
+
+ g_test_add_func("/sqlite3/schema_version/null",
+ test_sqlite3_get_schema_version_null);
+ g_test_add_func("/sqlite3/schema_version/new",
+ test_sqlite3_get_schema_version_new);
+
+ g_test_add_func("/sqlite3/string_migrations/null",
+ test_sqlite3_string_migrations_null);
+ g_test_add_func("/sqlite3/string_migrations/null-terminator",
+ test_sqlite3_string_migrations_null_terminator);
+ g_test_add_func("/sqlite3/string_migrations/multiple",
+ test_sqlite3_string_migrations_multiple);
+ g_test_add_func("/sqlite3/string_migrations/syntax-error",
+ test_sqlite3_string_migrations_syntax_error);
+ g_test_add_func("/sqlite3/string_migrations/older",
+ test_sqlite3_string_migrations_older);
+
+ g_test_add_func("/sqlite3/resource_migrations/null-path",
+ test_sqlite3_resource_migrations_null_path);
+ g_test_add_func("/sqlite3/resource_migrations/null-migrations",
+ test_sqlite3_resource_migrations_null_migrations);
+ g_test_add_func("/sqlite3/resource_migrations/null-terminator",
+ test_sqlite3_resource_migrations_null_terminator);
+ g_test_add_func("/sqlite3/resource_migrations/multiple",
+ test_sqlite3_resource_migrations_multiple);
+ g_test_add_func("/sqlite3/resource_migrations/missing",
+ test_sqlite3_resource_migrations_missing);
+ g_test_add_func("/sqlite3/resource_migrations/syntax-error",
+ test_sqlite3_resource_migrations_syntax_error);
+ g_test_add_func("/sqlite3/resource_migrations/older",
+ test_sqlite3_resource_migrations_older);
+
+ return g_test_run();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/sqlite3/test_sqlite3.gresource.xml Mon Oct 10 00:38:48 2022 -0500
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/im/pidgin/libpurple/tests/sqlite3/">
+ <file compressed="true">initial.sql</file>
+ <file compressed="true">secondary.sql</file>
+ <file compressed="true">malformed.sql</file>
+ </gresource>
+</gresources>