# HG changeset patch
# Node ID ca4993ee1fcc1efa1763f4a26e0645d972a5e649
# Parent  6d8ec5ef93790ba84b8bf2a85209c466881352ce
diff --git a/po/POTFILES b/po/POTFILES
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -1,2 +1,3 @@
+seagull/seagulldb.c
 seagull/seagullmigrations.c
 seagull/seagullversion.c
diff --git a/seagull/meson.build b/seagull/meson.build
--- a/seagull/meson.build
+++ b/seagull/meson.build
@@ -1,11 +1,13 @@
 SEAGULL_LIBRARY_VERSION = '0.1.0'
 
 SEAGULL_SOURCES = [
+  'seagulldb.c',
   'seagullmigrations.c',
   'seagullversion.c',
 ]
 
 SEAGULL_HEADERS = [
+  'seagulldb.h',
   'seagullmigrations.h',
   'seagulltypes.h',
   'seagullversion.h',
diff --git a/seagull/seagulldb.c b/seagull/seagulldb.c
new file mode 100644
--- /dev/null
+++ b/seagull/seagulldb.c
@@ -0,0 +1,108 @@
+/*
+ * 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 "seagulldb.h"
+
+G_DEFINE_QUARK(seagull-db-error, seagull_db_error)
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+SeagullSqlite3 *
+seagull_db_new_in_memory(GError **error) {
+	return seagull_db_new_from_file(":memory:", error);
+}
+
+SeagullSqlite3 *
+seagull_db_new_from_file(const char *filename, GError **error) {
+	SeagullSqlite3 *db = NULL;
+	int rc = SQLITE_OK;
+
+	g_return_val_if_fail(filename != NULL, NULL);
+	g_return_val_if_fail(filename[0] != '\0', NULL);
+
+	rc = sqlite3_open(filename, &db);
+	if(rc != SQLITE_OK) {
+		if(db != NULL) {
+			g_set_error(error,
+			            SEAGULL_DB_ERROR,
+			            SEAGULL_DB_ERROR_NEW_FAILED,
+			            "failed to create database: %s",
+			            sqlite3_errmsg(db));
+			g_clear_pointer(&db, sqlite3_close);
+		} else {
+			g_set_error_literal(error,
+			                    SEAGULL_DB_ERROR,
+			                    SEAGULL_DB_ERROR_NEW_FAILED,
+			                    "failed to create database: unknown error");
+		}
+	}
+
+	return db;
+}
+
+SeagullSqlite3 *
+seagull_db_new_from_uri(const char *uri, GError **error) {
+	SeagullSqlite3 *db = NULL;
+	int rc = SQLITE_OK;
+	int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI;
+
+	g_return_val_if_fail(uri != NULL, NULL);
+	g_return_val_if_fail(uri[0] != '\0', NULL);
+
+	rc = sqlite3_open_v2(uri, &db, flags, NULL);
+	if(rc != SQLITE_OK) {
+		if(db != NULL) {
+			g_set_error(error,
+			            SEAGULL_DB_ERROR,
+			            SEAGULL_DB_ERROR_NEW_FAILED,
+			            "failed to create database: %s",
+			            sqlite3_errmsg(db));
+			g_clear_pointer(&db, sqlite3_close);
+		} else {
+			g_set_error_literal(error,
+			                    SEAGULL_DB_ERROR,
+			                    SEAGULL_DB_ERROR_NEW_FAILED,
+			                    "failed to create database: unknown error");
+		}
+	}
+
+	return db;
+}
+
+gboolean
+seagull_db_close(SeagullSqlite3 *db, GError **error) {
+	int rc = SQLITE_OK;
+
+	rc = sqlite3_close(db);
+	if(rc == SQLITE_BUSY) {
+		g_set_error_literal(error,
+		                    SEAGULL_DB_ERROR,
+		                    SEAGULL_DB_ERROR_CLOSE_FAILED,
+		                    "the database is still in use");
+
+		return FALSE;
+	}
+
+	return TRUE;
+}
diff --git a/seagull/seagulldb.h b/seagull/seagulldb.h
new file mode 100644
--- /dev/null
+++ b/seagull/seagulldb.h
@@ -0,0 +1,131 @@
+/*
+ * 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_DB_H
+#define SEAGULL_DB_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "seagulltypes.h"
+#include "seagullversion.h"
+
+G_BEGIN_DECLS
+
+#define SEAGULL_DB_ERROR seagull_db_error_quark()
+
+/**
+ * SeagullDBError:
+ * @SEAGULL_DB_ERROR_NEW_FAILED: opening the database failed
+ * @SEAGULL_DB_ERROR_CLOSE_FAILED: the migration failed to execute
+ *
+ * Error codes returned when creating and closing databases.
+ *
+ * Since: 0.2
+ */
+typedef enum {
+	SEAGULL_DB_ERROR_NEW_FAILED SEAGULL_AVAILABLE_ENUMERATOR_IN_0_2,
+	SEAGULL_DB_ERROR_CLOSE_FAILED SEAGULL_AVAILABLE_ENUMERATOR_IN_0_2,
+} SeagullDBError;
+
+/**
+ * seagull_db_error_quark:
+ *
+ * The error domain to identify errors when opening and closing data bases.
+ *
+ * Returns: The error domain.
+ *
+ * Since: 0.2
+ */
+SEAGULL_AVAILABLE_IN_0_2
+GQuark seagull_db_error_quark(void);
+
+/**
+ * seagull_db_new_in_memory:
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Creates a new in memory database.
+ *
+ * Use [func@db_close] to close the database when done with it.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.2
+ */
+SEAGULL_AVAILABLE_IN_0_2
+SeagullSqlite3 *seagull_db_new_in_memory(GError **error);
+
+/**
+ * seagull_db_new_from_file:
+ * @filename: the filename to use
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Creates a new database with the given filename.
+ *
+ * Use [func@db_close] to close the database when done with it.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.2
+ */
+SEAGULL_AVAILABLE_IN_0_2
+SeagullSqlite3 *seagull_db_new_from_file(const char *filename, GError **error);
+
+/**
+ * seagull_db_new_from_uri:
+ * @uri: a URI to use
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Creates a new database from a URI.
+ *
+ * Internally this uses `sqlite3_open_v2` with the `SQLITE_OPEN_URI` flag which
+ * allows you to fine tune the database setup.
+ *
+ * Use [func@db_close] to close the database when done with it.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.2
+ */
+SEAGULL_AVAILABLE_IN_0_2
+SeagullSqlite3 *seagull_db_new_from_uri(const char *uri, GError **error);
+
+/**
+ * seagull_db_close:
+ * @db: (nullable): the database instance
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Closes a database instance.
+ *
+ * Returns: true on success; otherwise false with @error set.
+ *
+ * Since: 0.2
+ */
+SEAGULL_AVAILABLE_IN_0_2
+gboolean seagull_db_close(SeagullSqlite3 *db, GError **error);
+
+G_END_DECLS
+
+#endif /* SEAGULL_DB_H */
diff --git a/seagull/tests/meson.build b/seagull/tests/meson.build
--- a/seagull/tests/meson.build
+++ b/seagull/tests/meson.build
@@ -1,9 +1,10 @@
 PROGRAMS = [
+  'db',
 ]
 
 foreach PROGRAM : PROGRAMS
   e = executable(f'test_@PROGRAM@', f'test_@PROGRAM@.c',
-    dependencies : [seagull_dep])
+    dependencies : [seagull_dep, sqlite3_dep])
 
   test(PROGRAM, e)
 endforeach
diff --git a/seagull/tests/test_db.c b/seagull/tests/test_db.c
new file mode 100644
--- /dev/null
+++ b/seagull/tests/test_db.c
@@ -0,0 +1,133 @@
+/*
+ * 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 <seagull.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_seagull_db_new_in_memory(void) {
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_db_new_from_uri(void) {
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_from_uri("file:file.db?mode=memory", &error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_db_new_from_uri_failed(void) {
+	SeagullSqlite3 *db = NULL;
+	GError *error = NULL;
+
+	db = seagull_db_new_from_uri("file:file.db?mode=rw", &error);
+	g_assert_error(error,
+	               SEAGULL_DB_ERROR,
+	               SEAGULL_DB_ERROR_NEW_FAILED);
+	g_clear_error(&error);
+	g_assert_null(db);
+}
+
+static void
+test_seagull_db_close_null(void) {
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	result = seagull_db_close(NULL, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+static void
+test_seagull_db_close_in_use(void) {
+	SeagullSqlite3 *db = NULL;
+	sqlite3_stmt *statement = NULL;
+	GError *error = NULL;
+	int rc = SQLITE_OK;
+	gboolean result = FALSE;
+
+	db = seagull_db_new_in_memory(&error);
+	g_assert_no_error(error);
+	g_assert_nonnull(db);
+
+	/* Create a prepared statement to make the close fail. */
+	rc = sqlite3_prepare_v2(db, "SELECT 1", -1, &statement, NULL);
+	g_assert_nonnull(statement);
+	g_assert_cmpint(rc, ==, SQLITE_OK);
+
+	/* Verify that the close failed. */
+	result = seagull_db_close(db, &error);
+	g_assert_error(error,
+	               SEAGULL_DB_ERROR,
+	               SEAGULL_DB_ERROR_CLOSE_FAILED);
+	g_clear_error(&error);
+	g_assert_false(result);
+
+	/* Finish our statement. */
+	rc = sqlite3_finalize(statement);
+	g_assert_cmpint(rc, ==, SQLITE_OK);
+
+	/* Close the database successfully. */
+	result = seagull_db_close(db, &error);
+	g_assert_no_error(error);
+	g_assert_true(result);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/db/new/in-memory", test_seagull_db_new_in_memory);
+	g_test_add_func("/db/new/from-uri", test_seagull_db_new_from_uri);
+	g_test_add_func("/db/new/from-uri-failed",
+	                test_seagull_db_new_from_uri_failed);
+
+	g_test_add_func("/db/close/null", test_seagull_db_close_null);
+	g_test_add_func("/db/close/in-use", test_seagull_db_close_in_use);
+
+	return g_test_run();
+}
