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
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:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -229,7 +229,7 @@ jobs:
|
|||||||
# macOS Build Job (using same iOS project)
|
# macOS Build Job (using same iOS project)
|
||||||
build-macos:
|
build-macos:
|
||||||
name: Build macOS
|
name: Build macOS
|
||||||
runs-on: macos-15
|
runs-on: macos
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -294,7 +294,7 @@ jobs:
|
|||||||
# Linux Build Job
|
# Linux Build Job
|
||||||
build-linux:
|
build-linux:
|
||||||
name: Build Linux
|
name: Build Linux
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -326,7 +326,7 @@ jobs:
|
|||||||
# Summary Job
|
# Summary Job
|
||||||
build-summary:
|
build-summary:
|
||||||
name: Build Summary
|
name: Build Summary
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu
|
||||||
needs: [build-ios, build-macos, build-android, build-linux]
|
needs: [build-ios, build-macos, build-android, build-linux]
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,11 @@ models = files(
|
|||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
database = files(
|
database = files(
|
||||||
|
'src/database/db-error.vala',
|
||||||
'src/database/database.vala',
|
'src/database/database.vala',
|
||||||
'src/database/subscription-store.vala',
|
'src/database/subscription-store.vala',
|
||||||
'src/database/feed-item-store.vala',
|
'src/database/feed-item-store.vala',
|
||||||
'src/database/search-history-store.vala',
|
'src/database/search-history-store.vala',
|
||||||
'src/database/sqlite3.vapi',
|
|
||||||
'src/database/errors.vapi',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main library
|
# Main library
|
||||||
@@ -57,7 +56,7 @@ test_exe = executable('database-tests',
|
|||||||
'src/tests/database-tests.vala',
|
'src/tests/database-tests.vala',
|
||||||
dependencies: [glib_dep, gio_dep, json_dep, sqlite_dep, gobject_dep],
|
dependencies: [glib_dep, gio_dep, json_dep, sqlite_dep, gobject_dep],
|
||||||
link_with: [models_lib, database_lib],
|
link_with: [models_lib, database_lib],
|
||||||
vala_args: ['--vapidir', 'src/database', '--pkg', 'sqlite3'],
|
vala_args: ['--vapidir', '.', '--pkg', 'sqlite3'],
|
||||||
install: false
|
install: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
69
native-route/linux/rssuper-database.vapi
Normal file
69
native-route/linux/rssuper-database.vapi
Normal 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);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
* Database - Manages SQLite database connection and migrations
|
* Database - Manages SQLite database connection and migrations
|
||||||
*/
|
*/
|
||||||
public class RSSuper.Database : Object {
|
public class RSSuper.Database : Object {
|
||||||
private SQLite.DB db;
|
private Sqlite.Database db;
|
||||||
private string db_path;
|
private string db_path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,13 +48,13 @@ public class RSSuper.Database : Object {
|
|||||||
try {
|
try {
|
||||||
parent.make_directory_with_parents();
|
parent.make_directory_with_parents();
|
||||||
} catch (Error e) {
|
} 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);
|
int result = Sqlite.Database.open(db_path, out db);
|
||||||
if (result != SQLite.SQLITE_OK) {
|
if (result != Sqlite.OK) {
|
||||||
throw throw new Error.FAILED("Failed to open database: %s".printf(db.errmsg()));
|
throw new DBError.FAILED("Failed to open database: %s".printf(db.errmsg()));
|
||||||
}
|
}
|
||||||
|
|
||||||
execute("PRAGMA foreign_keys = ON;");
|
execute("PRAGMA foreign_keys = ON;");
|
||||||
@@ -90,7 +90,7 @@ public class RSSuper.Database : Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!schema_file.query_exists()) {
|
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;
|
uint8[] schema_bytes;
|
||||||
@@ -99,7 +99,7 @@ public class RSSuper.Database : Object {
|
|||||||
try {
|
try {
|
||||||
schema_file.load_contents(cancellable, out schema_bytes, out schema_str);
|
schema_file.load_contents(cancellable, out schema_bytes, out schema_str);
|
||||||
} catch (Error e) {
|
} 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;
|
string schema = schema_str ?? (string) schema_bytes;
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class RSSuper.Database : Object {
|
|||||||
debug("Database migrated to version %d", CURRENT_VERSION);
|
debug("Database migrated to version %d", CURRENT_VERSION);
|
||||||
|
|
||||||
} catch (Error e) {
|
} 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 {
|
private int get_current_version() throws Error {
|
||||||
try {
|
try {
|
||||||
SQLite.Stmt stmt;
|
Sqlite.Statement stmt;
|
||||||
int result = db.prepare_v2("SELECT COALESCE(MAX(version), 0) FROM schema_migrations;", -1, out stmt, null);
|
int result = db.prepare_v2("SELECT COALESCE(MAX(version), 0) FROM schema_migrations;", -1, out stmt, null);
|
||||||
|
|
||||||
if (result != SQLite.SQLITE_OK) {
|
if (result != Sqlite.OK) {
|
||||||
throw throw new Error.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
|
throw new DBError.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
|
||||||
}
|
}
|
||||||
|
|
||||||
int version = 0;
|
int version = 0;
|
||||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
if (stmt.step() == Sqlite.ROW) {
|
||||||
version = stmt.column_int(0);
|
version = stmt.column_int(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
|
|
||||||
} catch (Error e) {
|
} 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
|
* Execute a SQL statement
|
||||||
*/
|
*/
|
||||||
public void execute(string sql) throws Error {
|
public void execute(string sql) throws Error {
|
||||||
string errmsg = null;
|
string? errmsg;
|
||||||
int result = db.exec(sql, null, null, out errmsg);
|
int result = db.exec(sql, null, out errmsg);
|
||||||
|
|
||||||
if (result != SQLite.SQLITE_OK) {
|
if (result != Sqlite.OK) {
|
||||||
throw throw new Error.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
|
throw new DBError.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare a SQL statement
|
* Prepare a SQL statement
|
||||||
*/
|
*/
|
||||||
public SQLite.Stmt prepare(string sql) throws Error {
|
public Sqlite.Statement prepare(string sql) throws Error {
|
||||||
SQLite.Stmt stmt;
|
Sqlite.Statement stmt;
|
||||||
int result = db.prepare_v2(sql, -1, out stmt, null);
|
int result = db.prepare_v2(sql, -1, out stmt, null);
|
||||||
|
|
||||||
if (result != SQLite.SQLITE_OK) {
|
if (result != Sqlite.OK) {
|
||||||
throw throw new Error.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
|
throw new DBError.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
|
||||||
}
|
}
|
||||||
|
|
||||||
return stmt;
|
return stmt;
|
||||||
@@ -166,7 +166,7 @@ public class RSSuper.Database : Object {
|
|||||||
/**
|
/**
|
||||||
* Get the database connection handle
|
* Get the database connection handle
|
||||||
*/
|
*/
|
||||||
public SQLite.DB get_handle() {
|
public unowned Sqlite.Database get_handle() {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
24
native-route/linux/src/database/db-error.vala
Normal file
24
native-route/linux/src/database/db-error.vala
Normal 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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
|
|
||||||
stmt.bind_text(1, id, -1, null);
|
stmt.bind_text(1, id, -1, null);
|
||||||
|
|
||||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
if (stmt.step() == Sqlite.ROW) {
|
||||||
return row_to_item(stmt);
|
return row_to_item(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
|
|
||||||
stmt.bind_text(1, subscription_id, -1, null);
|
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);
|
var item = row_to_item(stmt);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
items.append(item);
|
items.append(item);
|
||||||
@@ -144,7 +144,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
"FROM feed_items ORDER BY published DESC LIMIT 1000;"
|
"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);
|
var item = row_to_item(stmt);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
items.append(item);
|
items.append(item);
|
||||||
@@ -174,7 +174,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
stmt.bind_text(1, query, -1, null);
|
stmt.bind_text(1, query, -1, null);
|
||||||
stmt.bind_int(2, limit);
|
stmt.bind_int(2, limit);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var item = row_to_item(stmt);
|
var item = row_to_item(stmt);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
items.append(item);
|
items.append(item);
|
||||||
@@ -242,7 +242,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
"ORDER BY published DESC LIMIT 100;"
|
"ORDER BY published DESC LIMIT 100;"
|
||||||
);
|
);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var item = row_to_item(stmt);
|
var item = row_to_item(stmt);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
items.append(item);
|
items.append(item);
|
||||||
@@ -266,7 +266,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
"ORDER BY published DESC LIMIT 100;"
|
"ORDER BY published DESC LIMIT 100;"
|
||||||
);
|
);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var item = row_to_item(stmt);
|
var item = row_to_item(stmt);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
items.append(item);
|
items.append(item);
|
||||||
@@ -326,7 +326,7 @@ public class RSSuper.FeedItemStore : Object {
|
|||||||
/**
|
/**
|
||||||
* Convert a database row to a FeedItem
|
* Convert a database row to a FeedItem
|
||||||
*/
|
*/
|
||||||
private FeedItem? row_to_item(SQLite.Stmt stmt) {
|
private FeedItem? row_to_item(Sqlite.Statement stmt) {
|
||||||
try {
|
try {
|
||||||
string categories_str = stmt.column_text(9);
|
string categories_str = stmt.column_text(9);
|
||||||
string[] categories = parse_categories(categories_str);
|
string[] categories = parse_categories(categories_str);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
|||||||
|
|
||||||
stmt.bind_int(1, limit);
|
stmt.bind_int(1, limit);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var query = row_to_query(stmt);
|
var query = row_to_query(stmt);
|
||||||
queries.append(query);
|
queries.append(query);
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
|||||||
|
|
||||||
stmt.bind_text(1, threshold, -1, null);
|
stmt.bind_text(1, threshold, -1, null);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var query = row_to_query(stmt);
|
var query = row_to_query(stmt);
|
||||||
queries.append(query);
|
queries.append(query);
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
|||||||
/**
|
/**
|
||||||
* Convert a database row to a SearchQuery
|
* 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 query_str = stmt.column_text(0);
|
||||||
string? filters_json = stmt.column_text(1);
|
string? filters_json = stmt.column_text(1);
|
||||||
string sort_str = stmt.column_text(2);
|
string sort_str = stmt.column_text(2);
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ namespace SQLite {
|
|||||||
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
|
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
|
||||||
public class DB {
|
public class DB {
|
||||||
[CCode (cname = "sqlite3_open")]
|
[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")]
|
[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")]
|
[CCode (cname = "sqlite3_errmsg")]
|
||||||
public unowned string errmsg();
|
public unowned string errmsg();
|
||||||
|
|
||||||
[CCode (cname = "sqlite3_prepare_v2")]
|
[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")]
|
[CCode (cname = "sqlite3_stmt", free_function = "sqlite3_finalize")]
|
||||||
@@ -47,6 +50,9 @@ namespace SQLite {
|
|||||||
|
|
||||||
[CCode (cname = "sqlite3_bind_null")]
|
[CCode (cname = "sqlite3_bind_null")]
|
||||||
public int bind_null(int i);
|
public int bind_null(int i);
|
||||||
|
|
||||||
|
[CCode (cname = "sqlite3_finalize")]
|
||||||
|
public int finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
[CCode (cname = "SQLITE_OK")]
|
[CCode (cname = "SQLITE_OK")]
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class RSSuper.SubscriptionStore : Object {
|
|||||||
|
|
||||||
stmt.bind_text(1, id, -1, null);
|
stmt.bind_text(1, id, -1, null);
|
||||||
|
|
||||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
if (stmt.step() == Sqlite.ROW) {
|
||||||
return row_to_subscription(stmt);
|
return row_to_subscription(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ public class RSSuper.SubscriptionStore : Object {
|
|||||||
"FROM feed_subscriptions ORDER BY title;"
|
"FROM feed_subscriptions ORDER BY title;"
|
||||||
);
|
);
|
||||||
|
|
||||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
while (stmt.step() == Sqlite.ROW) {
|
||||||
var subscription = row_to_subscription(stmt);
|
var subscription = row_to_subscription(stmt);
|
||||||
if (subscription != null) {
|
if (subscription != null) {
|
||||||
subscriptions.append(subscription);
|
subscriptions.append(subscription);
|
||||||
@@ -166,7 +166,7 @@ public class RSSuper.SubscriptionStore : Object {
|
|||||||
"FROM feed_subscriptions WHERE enabled = 1 ORDER BY title;"
|
"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);
|
var subscription = row_to_subscription(stmt);
|
||||||
if (subscription != null) {
|
if (subscription != null) {
|
||||||
subscriptions.append(subscription);
|
subscriptions.append(subscription);
|
||||||
@@ -194,7 +194,7 @@ public class RSSuper.SubscriptionStore : Object {
|
|||||||
|
|
||||||
stmt.bind_text(1, now_str, -1, null);
|
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);
|
var subscription = row_to_subscription(stmt);
|
||||||
if (subscription != null) {
|
if (subscription != null) {
|
||||||
subscriptions.append(subscription);
|
subscriptions.append(subscription);
|
||||||
@@ -207,7 +207,7 @@ public class RSSuper.SubscriptionStore : Object {
|
|||||||
/**
|
/**
|
||||||
* Convert a database row to a FeedSubscription
|
* Convert a database row to a FeedSubscription
|
||||||
*/
|
*/
|
||||||
private FeedSubscription? row_to_subscription(SQLite.Stmt stmt) {
|
private FeedSubscription? row_to_subscription(Sqlite.Statement stmt) {
|
||||||
try {
|
try {
|
||||||
var subscription = new FeedSubscription.with_values(
|
var subscription = new FeedSubscription.with_values(
|
||||||
stmt.column_text(0), // id
|
stmt.column_text(0), // id
|
||||||
|
|||||||
@@ -4,53 +4,20 @@
|
|||||||
* Unit tests for database layer.
|
* Unit tests for database layer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class RSSuper.DatabaseTests : TestCase {
|
public class RSSuper.DatabaseTests {
|
||||||
private Database? db;
|
private Database? db;
|
||||||
private string test_db_path;
|
private string test_db_path;
|
||||||
|
|
||||||
public override void setUp() {
|
public void run_subscription_crud() {
|
||||||
base.setUp();
|
|
||||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)Time.get_current_time());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||||
db = new Database(test_db_path);
|
db = new Database(test_db_path);
|
||||||
} catch (DatabaseError e) {
|
} catch (DBError e) {
|
||||||
warn("Failed to create test database: %s", e.message);
|
warning("Failed to create test database: %s", e.message);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void tearDown() {
|
|
||||||
base.tearDown();
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
file.delete();
|
|
||||||
} catch (DatabaseError e) {
|
|
||||||
warn("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 (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);
|
var store = new SubscriptionStore(db);
|
||||||
|
|
||||||
// Create test subscription
|
// Create test subscription
|
||||||
@@ -67,29 +34,55 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
|
|
||||||
// Test add
|
// Test add
|
||||||
store.add(subscription);
|
store.add(subscription);
|
||||||
assert_not_null(store.get_by_id("sub_1"));
|
var retrieved = store.get_by_id("sub_1");
|
||||||
|
if (retrieved == null) {
|
||||||
|
printerr("FAIL: Expected subscription to exist after add\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Test get
|
// Test get
|
||||||
var retrieved = store.get_by_id("sub_1");
|
if (retrieved.title != "Example Feed") {
|
||||||
assert_not_null(retrieved);
|
printerr("FAIL: Expected title 'Example Feed', got '%s'\n", retrieved.title);
|
||||||
assert_equal("Example Feed", retrieved.title);
|
return;
|
||||||
assert_equal("https://example.com/feed.xml", retrieved.url);
|
}
|
||||||
|
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
|
// Test update
|
||||||
retrieved.title = "Updated Feed";
|
retrieved.title = "Updated Feed";
|
||||||
store.update(retrieved);
|
store.update(retrieved);
|
||||||
var updated = store.get_by_id("sub_1");
|
var updated = store.get_by_id("sub_1");
|
||||||
assert_equal("Updated Feed", updated.title);
|
if (updated.title != "Updated Feed") {
|
||||||
|
printerr("FAIL: Expected title 'Updated Feed', got '%s'\n", updated.title);
|
||||||
// Test delete
|
return;
|
||||||
store.delete("sub_1");
|
|
||||||
var deleted = store.get_by_id("sub_1");
|
|
||||||
assert_null(deleted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void test_subscription_list() {
|
// Test delete
|
||||||
if (db == null) return;
|
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 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);
|
var store = new SubscriptionStore(db);
|
||||||
|
|
||||||
// Add multiple subscriptions
|
// Add multiple subscriptions
|
||||||
@@ -103,16 +96,34 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
|
|
||||||
// Test get_all
|
// Test get_all
|
||||||
var all = store.get_all();
|
var all = store.get_all();
|
||||||
assert_equal(3, all.length);
|
if (all.length != 3) {
|
||||||
|
printerr("FAIL: Expected 3 subscriptions, got %d\n", all.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Test get_enabled
|
// Test get_enabled
|
||||||
var enabled = store.get_enabled();
|
var enabled = store.get_enabled();
|
||||||
assert_equal(2, enabled.length);
|
if (enabled.length != 2) {
|
||||||
|
printerr("FAIL: Expected 2 enabled subscriptions, got %d\n", enabled.length);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void test_feed_item_crud() {
|
print("PASS: test_subscription_list\n");
|
||||||
if (db == null) return;
|
} 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 sub_store = new SubscriptionStore(db);
|
||||||
var item_store = new FeedItemStore(db);
|
var item_store = new FeedItemStore(db);
|
||||||
|
|
||||||
@@ -140,12 +151,21 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
// Test add
|
// Test add
|
||||||
item_store.add(item);
|
item_store.add(item);
|
||||||
var retrieved = item_store.get_by_id("item_1");
|
var retrieved = item_store.get_by_id("item_1");
|
||||||
assert_not_null(retrieved);
|
if (retrieved == null) {
|
||||||
assert_equal("Test Article", retrieved.title);
|
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
|
// Test get by subscription
|
||||||
var items = item_store.get_by_subscription("sub_1");
|
var items = item_store.get_by_subscription("sub_1");
|
||||||
assert_equal(1, items.length);
|
if (items.length != 1) {
|
||||||
|
printerr("FAIL: Expected 1 item, got %d\n", items.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Test mark as read
|
// Test mark as read
|
||||||
item_store.mark_as_read("item_1");
|
item_store.mark_as_read("item_1");
|
||||||
@@ -153,12 +173,27 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
// Test delete
|
// Test delete
|
||||||
item_store.delete("item_1");
|
item_store.delete("item_1");
|
||||||
var deleted = item_store.get_by_id("item_1");
|
var deleted = item_store.get_by_id("item_1");
|
||||||
assert_null(deleted);
|
if (deleted != null) {
|
||||||
|
printerr("FAIL: Expected item to be deleted\n");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void test_feed_item_batch() {
|
print("PASS: test_feed_item_crud\n");
|
||||||
if (db == null) return;
|
} 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 sub_store = new SubscriptionStore(db);
|
||||||
var item_store = new FeedItemStore(db);
|
var item_store = new FeedItemStore(db);
|
||||||
|
|
||||||
@@ -190,12 +225,27 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
item_store.add_batch(items);
|
item_store.add_batch(items);
|
||||||
|
|
||||||
var all = item_store.get_by_subscription("sub_1");
|
var all = item_store.get_by_subscription("sub_1");
|
||||||
assert_equal(5, all.length);
|
if (all.length != 5) {
|
||||||
|
printerr("FAIL: Expected 5 items, got %d\n", all.length);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void test_search_history() {
|
print("PASS: test_feed_item_batch\n");
|
||||||
if (db == null) return;
|
} 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);
|
var store = new SearchHistoryStore(db);
|
||||||
|
|
||||||
// Create test queries
|
// Create test queries
|
||||||
@@ -208,17 +258,38 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
|
|
||||||
// Test get_history
|
// Test get_history
|
||||||
var history = store.get_history();
|
var history = store.get_history();
|
||||||
assert_equal(2, history.length);
|
if (history.length != 2) {
|
||||||
assert_equal("another search", history[0].query); // Most recent first
|
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
|
// Test get_recent
|
||||||
var recent = store.get_recent();
|
var recent = store.get_recent();
|
||||||
assert_equal(2, recent.length);
|
if (recent.length != 2) {
|
||||||
|
printerr("FAIL: Expected 2 recent entries, got %d\n", recent.length);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void test_fts_search() {
|
print("PASS: test_search_history\n");
|
||||||
if (db == null) return;
|
} 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 sub_store = new SubscriptionStore(db);
|
||||||
var item_store = new FeedItemStore(db);
|
var item_store = new FeedItemStore(db);
|
||||||
|
|
||||||
@@ -262,27 +333,84 @@ public class RSSuper.DatabaseTests : TestCase {
|
|||||||
|
|
||||||
// Test FTS search
|
// Test FTS search
|
||||||
var results = item_store.search("swift");
|
var results = item_store.search("swift");
|
||||||
assert_equal(1, results.length);
|
if (results.length != 1) {
|
||||||
assert_equal("Swift Programming Guide", results[0].title);
|
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");
|
results = item_store.search("python");
|
||||||
assert_equal(1, results.length);
|
if (results.length != 1) {
|
||||||
assert_equal("Python for Data Science", results[0].title);
|
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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int main(string[] args) {
|
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();
|
print("\n=== Running subscription CRUD tests ===");
|
||||||
Test.add_func("/database/subscription_crud", test_case.test_subscription_crud);
|
tests.run_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);
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user