restructure
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 Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (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 Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
This commit is contained in:
200
linux/src/database/database.vala
Normal file
200
linux/src/database/database.vala
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Database.vala
|
||||
*
|
||||
* Core database connection and migration management for RSSuper Linux.
|
||||
* Uses SQLite with FTS5 for full-text search capabilities.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database - Manages SQLite database connection and migrations
|
||||
*/
|
||||
public class RSSuper.Database : Object {
|
||||
private Sqlite.Database db;
|
||||
private string db_path;
|
||||
|
||||
/**
|
||||
* Current database schema version
|
||||
*/
|
||||
public const int CURRENT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Signal emitted when database is ready
|
||||
*/
|
||||
public signal void ready();
|
||||
|
||||
/**
|
||||
* Signal emitted on error
|
||||
*/
|
||||
public signal void error(string message);
|
||||
|
||||
/**
|
||||
* Create a new database connection
|
||||
*
|
||||
* @param db_path Path to the SQLite database file
|
||||
*/
|
||||
public Database(string db_path) throws Error {
|
||||
this.db_path = db_path;
|
||||
this.open();
|
||||
this.migrate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open database connection
|
||||
*/
|
||||
private void open() throws Error {
|
||||
var file = File.new_for_path(db_path);
|
||||
var parent = file.get_parent();
|
||||
if (parent != null && !parent.query_exists()) {
|
||||
try {
|
||||
parent.make_directory_with_parents();
|
||||
} catch (Error e) {
|
||||
throw new DBError.FAILED("Failed to create database directory: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
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;");
|
||||
execute("PRAGMA journal_mode = WAL;");
|
||||
|
||||
debug("Database opened: %s", db_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run database migrations
|
||||
*/
|
||||
private void migrate() throws Error {
|
||||
// Create schema_migrations table if not exists
|
||||
execute("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')));");
|
||||
|
||||
// Create feed_subscriptions table
|
||||
execute("CREATE TABLE IF NOT EXISTS feed_subscriptions (id TEXT PRIMARY KEY, url TEXT NOT NULL UNIQUE, title TEXT NOT NULL, category TEXT, enabled INTEGER NOT NULL DEFAULT 1, fetch_interval INTEGER NOT NULL DEFAULT 60, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, last_fetched_at TEXT, next_fetch_at TEXT, error TEXT, http_auth_username TEXT, http_auth_password TEXT);");
|
||||
|
||||
// Create feed_items table
|
||||
execute("CREATE TABLE IF NOT EXISTS feed_items (id TEXT PRIMARY KEY, subscription_id TEXT NOT NULL, title TEXT NOT NULL, link TEXT, description TEXT, content TEXT, author TEXT, published TEXT, updated TEXT, categories TEXT, enclosure_url TEXT, enclosure_type TEXT, enclosure_length TEXT, guid TEXT, is_read INTEGER NOT NULL DEFAULT 0, is_starred INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (subscription_id) REFERENCES feed_subscriptions(id) ON DELETE CASCADE);");
|
||||
|
||||
// Create indexes for feed_items
|
||||
execute("CREATE INDEX IF NOT EXISTS idx_feed_items_subscription ON feed_items(subscription_id);");
|
||||
execute("CREATE INDEX IF NOT EXISTS idx_feed_items_published ON feed_items(published DESC);");
|
||||
execute("CREATE INDEX IF NOT EXISTS idx_feed_items_read ON feed_items(is_read);");
|
||||
execute("CREATE INDEX IF NOT EXISTS idx_feed_items_starred ON feed_items(is_starred);");
|
||||
|
||||
// Create search_history table
|
||||
execute("CREATE TABLE IF NOT EXISTS search_history (id INTEGER PRIMARY KEY AUTOINCREMENT, query TEXT NOT NULL, filters_json TEXT, sort_option TEXT NOT NULL DEFAULT 'relevance', page INTEGER NOT NULL DEFAULT 1, page_size INTEGER NOT NULL DEFAULT 20, result_count INTEGER, created_at TEXT NOT NULL DEFAULT (datetime('now')));");
|
||||
execute("CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at DESC);");
|
||||
|
||||
// Create FTS5 virtual table
|
||||
execute("CREATE VIRTUAL TABLE IF NOT EXISTS feed_items_fts USING fts5(title, description, content, author, content='feed_items', content_rowid='rowid');");
|
||||
|
||||
// Create triggers for FTS sync
|
||||
execute("CREATE TRIGGER IF NOT EXISTS feed_items_ai AFTER INSERT ON feed_items BEGIN INSERT INTO feed_items_fts(rowid, title, description, content, author) VALUES (new.rowid, new.title, new.description, new.content, new.author); END;");
|
||||
execute("CREATE TRIGGER IF NOT EXISTS feed_items_ad AFTER DELETE ON feed_items BEGIN INSERT INTO feed_items_fts(feed_items_fts, rowid, title, description, content, author) VALUES('delete', old.rowid, old.title, old.description, old.content, old.author); END;");
|
||||
execute("CREATE TRIGGER IF NOT EXISTS feed_items_au AFTER UPDATE ON feed_items BEGIN INSERT INTO feed_items_fts(feed_items_fts, rowid, title, description, content, author) VALUES('delete', old.rowid, old.title, old.description, old.content, old.author); INSERT INTO feed_items_fts(rowid, title, description, content, author) VALUES (new.rowid, new.title, new.description, new.content, new.author); END;");
|
||||
|
||||
// Record migration
|
||||
execute("INSERT OR REPLACE INTO schema_migrations (version, applied_at) VALUES (" + CURRENT_VERSION.to_string() + ", datetime('now'));");
|
||||
|
||||
debug("Database migrated to version %d", CURRENT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current migration version
|
||||
*/
|
||||
private int get_current_version() throws Error {
|
||||
try {
|
||||
Sqlite.Statement stmt;
|
||||
int result = db.prepare_v2("SELECT COALESCE(MAX(version), 0) FROM schema_migrations;", -1, out stmt, null);
|
||||
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
|
||||
}
|
||||
|
||||
int version = 0;
|
||||
if (stmt.step() == Sqlite.ROW) {
|
||||
version = stmt.column_int(0);
|
||||
}
|
||||
|
||||
return version;
|
||||
|
||||
} catch (Error e) {
|
||||
throw new DBError.FAILED("Failed to get migration version: %s".printf(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SQL statement
|
||||
*/
|
||||
public void execute(string sql) throws Error {
|
||||
string? errmsg;
|
||||
int result = db.exec(sql, null, out errmsg);
|
||||
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a SQL statement
|
||||
*/
|
||||
public Sqlite.Statement prepare(string sql) throws Error {
|
||||
Sqlite.Statement stmt;
|
||||
int result = db.prepare_v2(sql, -1, out stmt, null);
|
||||
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
|
||||
}
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database connection handle
|
||||
*/
|
||||
public unowned Sqlite.Database get_handle() {
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
public void close() {
|
||||
if (db != null) {
|
||||
db = null;
|
||||
debug("Database closed: %s", db_path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a transaction
|
||||
*/
|
||||
public void begin_transaction() throws Error {
|
||||
execute("BEGIN TRANSACTION;");
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit a transaction
|
||||
*/
|
||||
public void commit() throws Error {
|
||||
execute("COMMIT;");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback a transaction
|
||||
*/
|
||||
public void rollback() throws Error {
|
||||
execute("ROLLBACK;");
|
||||
}
|
||||
|
||||
/* Helper to convert GLib.List to array */
|
||||
private T[] toArray<T>(GLib.List<T> list) {
|
||||
T[] arr = {};
|
||||
for (unowned var node = list; node != null; node = node.next) {
|
||||
arr += node.data;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user