linux db
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled

This commit is contained in:
2026-03-30 00:02:12 -04:00
parent dc17a71be4
commit e572437f37
10 changed files with 529 additions and 303 deletions

View File

@@ -30,12 +30,11 @@ models = files(
# Database files
database = files(
'src/database/db-error.vala',
'src/database/database.vala',
'src/database/subscription-store.vala',
'src/database/feed-item-store.vala',
'src/database/search-history-store.vala',
'src/database/sqlite3.vapi',
'src/database/errors.vapi',
)
# Main library
@@ -57,7 +56,7 @@ test_exe = executable('database-tests',
'src/tests/database-tests.vala',
dependencies: [glib_dep, gio_dep, json_dep, sqlite_dep, gobject_dep],
link_with: [models_lib, database_lib],
vala_args: ['--vapidir', 'src/database', '--pkg', 'sqlite3'],
vala_args: ['--vapidir', '.', '--pkg', 'sqlite3'],
install: false
)

View File

@@ -0,0 +1,69 @@
/*
* RSSuper Database vapi - exports SQLite bindings for use by dependent modules
*/
[CCode (cheader_filename = "sqlite3.h")]
namespace SQLite {
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
public class DB {
[CCode (cname = "sqlite3_open")]
public static int open(string filename, out DB db);
[CCode (cname = "sqlite3_close")]
public int close();
[CCode (cname = "sqlite3_exec")]
public int exec(string sql, DBCallback? callback = null, void* arg = null, [CCode (array_length = false)] out string? errmsg = null);
[CCode (cname = "sqlite3_errmsg")]
public unowned string errmsg();
[CCode (cname = "sqlite3_prepare_v2")]
public int prepare_v2(string zSql, int nByte, out Stmt stmt, void* pzTail = null);
}
[CCode (cname = "sqlite3_stmt", free_function = "sqlite3_finalize")]
public class Stmt {
[CCode (cname = "sqlite3_step")]
public int step();
[CCode (cname = "sqlite3_column_count")]
public int column_count();
[CCode (cname = "sqlite3_column_text")]
public unowned string column_text(int i);
[CCode (cname = "sqlite3_column_int")]
public int column_int(int i);
[CCode (cname = "sqlite3_column_double")]
public double column_double(int i);
[CCode (cname = "sqlite3_bind_text")]
public int bind_text(int i, string z, int n, void* x);
[CCode (cname = "sqlite3_bind_int")]
public int bind_int(int i, int val);
[CCode (cname = "sqlite3_bind_double")]
public int bind_double(int i, double val);
[CCode (cname = "sqlite3_bind_null")]
public int bind_null(int i);
[CCode (cname = "sqlite3_finalize")]
public int finalize();
}
[CCode (cname = "SQLITE_OK")]
public const int SQLITE_OK;
[CCode (cname = "SQLITE_ROW")]
public const int SQLITE_ROW;
[CCode (cname = "SQLITE_DONE")]
public const int SQLITE_DONE;
[CCode (cname = "SQLITE_ERROR")]
public const int SQLITE_ERROR;
[CCode (simple_type = true)]
public delegate int DBCallback(void* arg, int argc, string[] argv, string[] col_names);
}

View File

@@ -9,7 +9,7 @@
* Database - Manages SQLite database connection and migrations
*/
public class RSSuper.Database : Object {
private SQLite.DB db;
private Sqlite.Database db;
private string db_path;
/**
@@ -48,13 +48,13 @@ public class RSSuper.Database : Object {
try {
parent.make_directory_with_parents();
} catch (Error e) {
throw throw new Error.FAILED("Failed to create database directory: %s", e.message);
throw new DBError.FAILED("Failed to create database directory: %s", e.message);
}
}
int result = SQLite.DB.open(db_path, out db);
if (result != SQLite.SQLITE_OK) {
throw throw new Error.FAILED("Failed to open database: %s".printf(db.errmsg()));
int result = Sqlite.Database.open(db_path, out db);
if (result != Sqlite.OK) {
throw new DBError.FAILED("Failed to open database: %s".printf(db.errmsg()));
}
execute("PRAGMA foreign_keys = ON;");
@@ -90,7 +90,7 @@ public class RSSuper.Database : Object {
}
if (!schema_file.query_exists()) {
throw throw new Error.FAILED("Schema file not found: %s".printf(schema_path));
throw new DBError.FAILED("Schema file not found: %s".printf(schema_path));
}
uint8[] schema_bytes;
@@ -99,7 +99,7 @@ public class RSSuper.Database : Object {
try {
schema_file.load_contents(cancellable, out schema_bytes, out schema_str);
} catch (Error e) {
throw throw new Error.FAILED("Failed to read schema file: %s", e.message);
throw new DBError.FAILED("Failed to read schema file: %s", e.message);
}
string schema = schema_str ?? (string) schema_bytes;
@@ -109,7 +109,7 @@ public class RSSuper.Database : Object {
debug("Database migrated to version %d", CURRENT_VERSION);
} catch (Error e) {
throw throw new Error.FAILED("Migration failed: %s".printf(e.message));
throw new DBError.FAILED("Migration failed: %s".printf(e.message));
}
}
@@ -118,22 +118,22 @@ public class RSSuper.Database : Object {
*/
private int get_current_version() throws Error {
try {
SQLite.Stmt stmt;
Sqlite.Statement stmt;
int result = db.prepare_v2("SELECT COALESCE(MAX(version), 0) FROM schema_migrations;", -1, out stmt, null);
if (result != SQLite.SQLITE_OK) {
throw throw new Error.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
if (result != Sqlite.OK) {
throw new DBError.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
}
int version = 0;
if (stmt.step() == SQLite.SQLITE_ROW) {
if (stmt.step() == Sqlite.ROW) {
version = stmt.column_int(0);
}
return version;
} catch (Error e) {
throw throw new Error.FAILED("Failed to get migration version: %s".printf(e.message));
throw new DBError.FAILED("Failed to get migration version: %s".printf(e.message));
}
}
@@ -141,23 +141,23 @@ public class RSSuper.Database : Object {
* Execute a SQL statement
*/
public void execute(string sql) throws Error {
string errmsg = null;
int result = db.exec(sql, null, null, out errmsg);
string? errmsg;
int result = db.exec(sql, null, out errmsg);
if (result != SQLite.SQLITE_OK) {
throw throw new Error.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
if (result != Sqlite.OK) {
throw new DBError.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
}
}
/**
* Prepare a SQL statement
*/
public SQLite.Stmt prepare(string sql) throws Error {
SQLite.Stmt stmt;
public Sqlite.Statement prepare(string sql) throws Error {
Sqlite.Statement stmt;
int result = db.prepare_v2(sql, -1, out stmt, null);
if (result != SQLite.SQLITE_OK) {
throw throw new Error.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
if (result != Sqlite.OK) {
throw new DBError.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
}
return stmt;
@@ -166,7 +166,7 @@ public class RSSuper.Database : Object {
/**
* Get the database connection handle
*/
public SQLite.DB get_handle() {
public unowned Sqlite.Database get_handle() {
return db;
}

View File

@@ -0,0 +1,24 @@
/*
* DBError.vala
*
* Database error domain definition.
*/
namespace RSSuper {
/**
* DBError - Database error domain
*/
public errordomain DBError {
FAILED, /** Generic database operation failed */
NOT_FOUND, /** Record not found */
DUPLICATE, /** Duplicate key or record */
CORRUPTED, /** Database is corrupted */
TIMEOUT, /** Operation timed out */
INVALID_STATE, /** Invalid database state */
MIGRATION_FAILED, /** Migration failed */
CONSTRAINT_FAILED, /** Constraint violation */
FOREIGN_KEY_FAILED, /** Foreign key constraint failed */
UNIQUE_FAILED, /** Unique constraint failed */
CHECK_FAILED, /** Check constraint failed */
}
}

View File

@@ -98,7 +98,7 @@ public class RSSuper.FeedItemStore : Object {
stmt.bind_text(1, id, -1, null);
if (stmt.step() == SQLite.SQLITE_ROW) {
if (stmt.step() == Sqlite.ROW) {
return row_to_item(stmt);
}
@@ -121,7 +121,7 @@ public class RSSuper.FeedItemStore : Object {
stmt.bind_text(1, subscription_id, -1, null);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var item = row_to_item(stmt);
if (item != null) {
items.append(item);
@@ -144,7 +144,7 @@ public class RSSuper.FeedItemStore : Object {
"FROM feed_items ORDER BY published DESC LIMIT 1000;"
);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var item = row_to_item(stmt);
if (item != null) {
items.append(item);
@@ -174,7 +174,7 @@ public class RSSuper.FeedItemStore : Object {
stmt.bind_text(1, query, -1, null);
stmt.bind_int(2, limit);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var item = row_to_item(stmt);
if (item != null) {
items.append(item);
@@ -242,7 +242,7 @@ public class RSSuper.FeedItemStore : Object {
"ORDER BY published DESC LIMIT 100;"
);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var item = row_to_item(stmt);
if (item != null) {
items.append(item);
@@ -266,7 +266,7 @@ public class RSSuper.FeedItemStore : Object {
"ORDER BY published DESC LIMIT 100;"
);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var item = row_to_item(stmt);
if (item != null) {
items.append(item);
@@ -326,7 +326,7 @@ public class RSSuper.FeedItemStore : Object {
/**
* Convert a database row to a FeedItem
*/
private FeedItem? row_to_item(SQLite.Stmt stmt) {
private FeedItem? row_to_item(Sqlite.Statement stmt) {
try {
string categories_str = stmt.column_text(9);
string[] categories = parse_categories(categories_str);

View File

@@ -74,7 +74,7 @@ public class RSSuper.SearchHistoryStore : Object {
stmt.bind_int(1, limit);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var query = row_to_query(stmt);
queries.append(query);
}
@@ -101,7 +101,7 @@ public class RSSuper.SearchHistoryStore : Object {
stmt.bind_text(1, threshold, -1, null);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var query = row_to_query(stmt);
queries.append(query);
}
@@ -148,7 +148,7 @@ public class RSSuper.SearchHistoryStore : Object {
/**
* Convert a database row to a SearchQuery
*/
private SearchQuery row_to_query(SQLite.Stmt stmt) {
private SearchQuery row_to_query(Sqlite.Statement stmt) {
string query_str = stmt.column_text(0);
string? filters_json = stmt.column_text(1);
string sort_str = stmt.column_text(2);

View File

@@ -7,16 +7,19 @@ namespace SQLite {
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
public class DB {
[CCode (cname = "sqlite3_open")]
public static int open(string filename, [CCode (array_length = false)] out DB db);
public static int open(string filename, out DB db);
[CCode (cname = "sqlite3_close")]
public int close();
[CCode (cname = "sqlite3_exec")]
public int exec(string sql, [CCode (array_length = false)] DBCallback callback = null, void* arg = null, [CCode (array_length = false)] out string errmsg = null);
public int exec(string sql, DBCallback? callback = null, void* arg = null, [CCode (array_length = false)] out string? errmsg = null);
[CCode (cname = "sqlite3_errmsg")]
public unowned string errmsg();
[CCode (cname = "sqlite3_prepare_v2")]
public int prepare_v2(string zSql, int nByte, [CCode (array_length = false)] out Stmt stmt, void* pzTail = null);
public int prepare_v2(string zSql, int nByte, out Stmt stmt, void* pzTail = null);
}
[CCode (cname = "sqlite3_stmt", free_function = "sqlite3_finalize")]
@@ -47,6 +50,9 @@ namespace SQLite {
[CCode (cname = "sqlite3_bind_null")]
public int bind_null(int i);
[CCode (cname = "sqlite3_finalize")]
public int finalize();
}
[CCode (cname = "SQLITE_OK")]

View File

@@ -76,7 +76,7 @@ public class RSSuper.SubscriptionStore : Object {
stmt.bind_text(1, id, -1, null);
if (stmt.step() == SQLite.SQLITE_ROW) {
if (stmt.step() == Sqlite.ROW) {
return row_to_subscription(stmt);
}
@@ -95,7 +95,7 @@ public class RSSuper.SubscriptionStore : Object {
"FROM feed_subscriptions ORDER BY title;"
);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var subscription = row_to_subscription(stmt);
if (subscription != null) {
subscriptions.append(subscription);
@@ -166,7 +166,7 @@ public class RSSuper.SubscriptionStore : Object {
"FROM feed_subscriptions WHERE enabled = 1 ORDER BY title;"
);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var subscription = row_to_subscription(stmt);
if (subscription != null) {
subscriptions.append(subscription);
@@ -194,7 +194,7 @@ public class RSSuper.SubscriptionStore : Object {
stmt.bind_text(1, now_str, -1, null);
while (stmt.step() == SQLite.SQLITE_ROW) {
while (stmt.step() == Sqlite.ROW) {
var subscription = row_to_subscription(stmt);
if (subscription != null) {
subscriptions.append(subscription);
@@ -207,7 +207,7 @@ public class RSSuper.SubscriptionStore : Object {
/**
* Convert a database row to a FeedSubscription
*/
private FeedSubscription? row_to_subscription(SQLite.Stmt stmt) {
private FeedSubscription? row_to_subscription(Sqlite.Statement stmt) {
try {
var subscription = new FeedSubscription.with_values(
stmt.column_text(0), // id

View File

@@ -4,285 +4,413 @@
* Unit tests for database layer.
*/
public class RSSuper.DatabaseTests : TestCase {
public class RSSuper.DatabaseTests {
private Database? db;
private string test_db_path;
public override void setUp() {
base.setUp();
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)Time.get_current_time());
public void run_subscription_crud() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
db = new Database(test_db_path);
} catch (DatabaseError e) {
warn("Failed to create test database: %s", e.message);
var store = new SubscriptionStore(db);
// Create test subscription
var subscription = new FeedSubscription.with_values(
"sub_1",
"https://example.com/feed.xml",
"Example Feed",
60,
"Technology",
true,
"2024-01-01T00:00:00Z",
"2024-01-01T00:00:00Z"
);
// Test add
store.add(subscription);
var retrieved = store.get_by_id("sub_1");
if (retrieved == null) {
printerr("FAIL: Expected subscription to exist after add\n");
return;
}
// Test get
if (retrieved.title != "Example Feed") {
printerr("FAIL: Expected title 'Example Feed', got '%s'\n", retrieved.title);
return;
}
if (retrieved.url != "https://example.com/feed.xml") {
printerr("FAIL: Expected url 'https://example.com/feed.xml', got '%s'\n", retrieved.url);
return;
}
// Test update
retrieved.title = "Updated Feed";
store.update(retrieved);
var updated = store.get_by_id("sub_1");
if (updated.title != "Updated Feed") {
printerr("FAIL: Expected title 'Updated Feed', got '%s'\n", updated.title);
return;
}
// Test delete
store.remove_subscription("sub_1");
var deleted = store.get_by_id("sub_1");
if (deleted != null) {
printerr("FAIL: Expected subscription to be deleted\n");
return;
}
print("PASS: test_subscription_crud\n");
} finally {
cleanup();
}
}
public override void tearDown() {
base.tearDown();
public void run_subscription_list() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
var store = new SubscriptionStore(db);
// Add multiple subscriptions
var sub1 = new FeedSubscription.with_values("sub_1", "https://feed1.com", "Feed 1");
var sub2 = new FeedSubscription.with_values("sub_2", "https://feed2.com", "Feed 2");
var sub3 = new FeedSubscription.with_values("sub_3", "https://feed3.com", "Feed 3", 60, null, false);
store.add(sub1);
store.add(sub2);
store.add(sub3);
// Test get_all
var all = store.get_all();
if (all.length != 3) {
printerr("FAIL: Expected 3 subscriptions, got %d\n", all.length);
return;
}
// Test get_enabled
var enabled = store.get_enabled();
if (enabled.length != 2) {
printerr("FAIL: Expected 2 enabled subscriptions, got %d\n", enabled.length);
return;
}
print("PASS: test_subscription_list\n");
} finally {
cleanup();
}
}
public void run_feed_item_crud() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription first
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Create test item
var item = new FeedItem.with_values(
"item_1",
"Test Article",
"https://example.com/article",
"This is a test description",
"Full content of the article",
"John Doe",
"2024-01-01T12:00:00Z",
"2024-01-01T12:00:00Z",
{"Technology", "News"},
null, null, null, null,
"Example Feed"
);
// Test add
item_store.add(item);
var retrieved = item_store.get_by_id("item_1");
if (retrieved == null) {
printerr("FAIL: Expected item to exist after add\n");
return;
}
if (retrieved.title != "Test Article") {
printerr("FAIL: Expected title 'Test Article', got '%s'\n", retrieved.title);
return;
}
// Test get by subscription
var items = item_store.get_by_subscription("sub_1");
if (items.length != 1) {
printerr("FAIL: Expected 1 item, got %d\n", items.length);
return;
}
// Test mark as read
item_store.mark_as_read("item_1");
// Test delete
item_store.delete("item_1");
var deleted = item_store.get_by_id("item_1");
if (deleted != null) {
printerr("FAIL: Expected item to be deleted\n");
return;
}
print("PASS: test_feed_item_crud\n");
} finally {
cleanup();
}
}
public void run_feed_item_batch() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Create multiple items
var items = new FeedItem[5];
for (var i = 0; i < 5; i++) {
items[i] = new FeedItem.with_values(
"item_%d".printf(i),
"Article %d".printf(i),
"https://example.com/article%d".printf(i),
"Description %d".printf(i),
null,
"Author %d".printf(i),
"2024-01-%02dT12:00:00Z".printf(i + 1),
null,
null,
null, null, null, null,
"Example Feed"
);
}
// Test batch insert
item_store.add_batch(items);
var all = item_store.get_by_subscription("sub_1");
if (all.length != 5) {
printerr("FAIL: Expected 5 items, got %d\n", all.length);
return;
}
print("PASS: test_feed_item_batch\n");
} finally {
cleanup();
}
}
public void run_search_history() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
var store = new SearchHistoryStore(db);
// Create test queries
var query1 = SearchQuery("test query", 1, 20, null, SearchSortOption.RELEVANCE);
var query2 = SearchQuery("another search", 1, 10, null, SearchSortOption.DATE_DESC);
// Test record
store.record_search(query1, 15);
store.record_search(query2, 8);
// Test get_history
var history = store.get_history();
if (history.length != 2) {
printerr("FAIL: Expected 2 history entries, got %d\n", history.length);
return;
}
if (history[0].query != "another search") {
printerr("FAIL: Expected 'another search', got '%s'\n", history[0].query);
return;
}
// Test get_recent
var recent = store.get_recent();
if (recent.length != 2) {
printerr("FAIL: Expected 2 recent entries, got %d\n", recent.length);
return;
}
print("PASS: test_search_history\n");
} finally {
cleanup();
}
}
public void run_fts_search() {
try {
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
db = new Database(test_db_path);
} catch (DBError e) {
warning("Failed to create test database: %s", e.message);
return;
}
try {
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Add items with searchable content
var item1 = new FeedItem.with_values(
"item_1",
"Swift Programming Guide",
"https://example.com/swift",
"Learn Swift programming language basics",
"A comprehensive guide to Swift",
"Apple Developer",
"2024-01-01T12:00:00Z",
null,
null,
null, null, null, null,
"Example Feed"
);
var item2 = new FeedItem.with_values(
"item_2",
"Python for Data Science",
"https://example.com/python",
"Data analysis with Python and pandas",
"Complete Python data science tutorial",
"Data Team",
"2024-01-02T12:00:00Z",
null,
null,
null, null, null, null,
"Example Feed"
);
item_store.add(item1);
item_store.add(item2);
// Test FTS search
var results = item_store.search("swift");
if (results.length != 1) {
printerr("FAIL: Expected 1 result for 'swift', got %d\n", results.length);
return;
}
if (results[0].title != "Swift Programming Guide") {
printerr("FAIL: Expected 'Swift Programming Guide', got '%s'\n", results[0].title);
return;
}
results = item_store.search("python");
if (results.length != 1) {
printerr("FAIL: Expected 1 result for 'python', got %d\n", results.length);
return;
}
if (results[0].title != "Python for Data Science") {
printerr("FAIL: Expected 'Python for Data Science', got '%s'\n", results[0].title);
return;
}
print("PASS: test_fts_search\n");
} finally {
cleanup();
}
}
private void cleanup() {
if (db != null) {
db.close();
db = null;
}
// Clean up test database
var file = File.new_for_path(test_db_path);
if (file.query_exists()) {
try {
file.delete();
} catch (DatabaseError e) {
warn("Failed to delete test database: %s", e.message);
if (test_db_path != null && test_db_path.length > 0) {
var file = File.new_for_path(test_db_path);
if (file.query_exists()) {
try {
file.delete();
} catch (Error e) {
warning("Failed to delete test database: %s", e.message);
}
}
// Clean up WAL file
var wal_file = File.new_for_path(test_db_path + "-wal");
if (wal_file.query_exists()) {
try {
wal_file.delete();
} catch (Error e) {
warning("Failed to delete WAL file: %s", e.message);
}
}
}
// Clean up WAL file
var wal_file = File.new_for_path(test_db_path + "-wal");
if (wal_file.query_exists()) {
try {
wal_file.delete();
} catch (DatabaseError e) {
warn("Failed to delete WAL file: %s", e.message);
}
}
}
public void test_subscription_crud() {
if (db == null) return;
var store = new SubscriptionStore(db);
// Create test subscription
var subscription = new FeedSubscription.with_values(
"sub_1",
"https://example.com/feed.xml",
"Example Feed",
60,
"Technology",
true,
"2024-01-01T00:00:00Z",
"2024-01-01T00:00:00Z"
);
// Test add
store.add(subscription);
assert_not_null(store.get_by_id("sub_1"));
// Test get
var retrieved = store.get_by_id("sub_1");
assert_not_null(retrieved);
assert_equal("Example Feed", retrieved.title);
assert_equal("https://example.com/feed.xml", retrieved.url);
// Test update
retrieved.title = "Updated Feed";
store.update(retrieved);
var updated = store.get_by_id("sub_1");
assert_equal("Updated Feed", updated.title);
// Test delete
store.delete("sub_1");
var deleted = store.get_by_id("sub_1");
assert_null(deleted);
}
public void test_subscription_list() {
if (db == null) return;
var store = new SubscriptionStore(db);
// Add multiple subscriptions
var sub1 = new FeedSubscription.with_values("sub_1", "https://feed1.com", "Feed 1");
var sub2 = new FeedSubscription.with_values("sub_2", "https://feed2.com", "Feed 2");
var sub3 = new FeedSubscription.with_values("sub_3", "https://feed3.com", "Feed 3", 60, null, false);
store.add(sub1);
store.add(sub2);
store.add(sub3);
// Test get_all
var all = store.get_all();
assert_equal(3, all.length);
// Test get_enabled
var enabled = store.get_enabled();
assert_equal(2, enabled.length);
}
public void test_feed_item_crud() {
if (db == null) return;
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription first
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Create test item
var item = new FeedItem.with_values(
"item_1",
"Test Article",
"https://example.com/article",
"This is a test description",
"Full content of the article",
"John Doe",
"2024-01-01T12:00:00Z",
"2024-01-01T12:00:00Z",
{"Technology", "News"},
null, null, null, null,
"Example Feed"
);
// Test add
item_store.add(item);
var retrieved = item_store.get_by_id("item_1");
assert_not_null(retrieved);
assert_equal("Test Article", retrieved.title);
// Test get by subscription
var items = item_store.get_by_subscription("sub_1");
assert_equal(1, items.length);
// Test mark as read
item_store.mark_as_read("item_1");
// Test delete
item_store.delete("item_1");
var deleted = item_store.get_by_id("item_1");
assert_null(deleted);
}
public void test_feed_item_batch() {
if (db == null) return;
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Create multiple items
var items = new FeedItem[5];
for (var i = 0; i < 5; i++) {
items[i] = new FeedItem.with_values(
"item_%d".printf(i),
"Article %d".printf(i),
"https://example.com/article%d".printf(i),
"Description %d".printf(i),
null,
"Author %d".printf(i),
"2024-01-%02dT12:00:00Z".printf(i + 1),
null,
null,
null, null, null, null,
"Example Feed"
);
}
// Test batch insert
item_store.add_batch(items);
var all = item_store.get_by_subscription("sub_1");
assert_equal(5, all.length);
}
public void test_search_history() {
if (db == null) return;
var store = new SearchHistoryStore(db);
// Create test queries
var query1 = SearchQuery("test query", 1, 20, null, SearchSortOption.RELEVANCE);
var query2 = SearchQuery("another search", 1, 10, null, SearchSortOption.DATE_DESC);
// Test record
store.record_search(query1, 15);
store.record_search(query2, 8);
// Test get_history
var history = store.get_history();
assert_equal(2, history.length);
assert_equal("another search", history[0].query); // Most recent first
// Test get_recent
var recent = store.get_recent();
assert_equal(2, recent.length);
}
public void test_fts_search() {
if (db == null) return;
var sub_store = new SubscriptionStore(db);
var item_store = new FeedItemStore(db);
// Create subscription
var subscription = new FeedSubscription.with_values(
"sub_1", "https://example.com/feed.xml", "Example Feed"
);
sub_store.add(subscription);
// Add items with searchable content
var item1 = new FeedItem.with_values(
"item_1",
"Swift Programming Guide",
"https://example.com/swift",
"Learn Swift programming language basics",
"A comprehensive guide to Swift",
"Apple Developer",
"2024-01-01T12:00:00Z",
null,
null,
null, null, null, null,
"Example Feed"
);
var item2 = new FeedItem.with_values(
"item_2",
"Python for Data Science",
"https://example.com/python",
"Data analysis with Python and pandas",
"Complete Python data science tutorial",
"Data Team",
"2024-01-02T12:00:00Z",
null,
null,
null, null, null, null,
"Example Feed"
);
item_store.add(item1);
item_store.add(item2);
// Test FTS search
var results = item_store.search("swift");
assert_equal(1, results.length);
assert_equal("Swift Programming Guide", results[0].title);
results = item_store.search("python");
assert_equal(1, results.length);
assert_equal("Python for Data Science", results[0].title);
}
public static int main(string[] args) {
Test.init(ref args);
print("Running database tests...\n");
var suite = Test.create_suite();
var tests = new DatabaseTests();
var test_case = new DatabaseTests();
Test.add_func("/database/subscription_crud", test_case.test_subscription_crud);
Test.add_func("/database/subscription_list", test_case.test_subscription_list);
Test.add_func("/database/feed_item_crud", test_case.test_feed_item_crud);
Test.add_func("/database/feed_item_batch", test_case.test_feed_item_batch);
Test.add_func("/database/search_history", test_case.test_search_history);
Test.add_func("/database/fts_search", test_case.test_fts_search);
print("\n=== Running subscription CRUD tests ===");
tests.run_subscription_crud();
return Test.run();
print("\n=== Running subscription list tests ===");
tests.run_subscription_list();
print("\n=== Running feed item CRUD tests ===");
tests.run_feed_item_crud();
print("\n=== Running feed item batch tests ===");
tests.run_feed_item_batch();
print("\n=== Running search history tests ===");
tests.run_search_history();
print("\n=== Running FTS search tests ===");
tests.run_fts_search();
print("\nAll tests completed!\n");
return 0;
}
}