readr

Minimal Terminal RSS Reader
Log | Files | Refs | README | LICENSE

db.c (6034B)




#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sqlite3.h>

#include "db.h"
#include "utils.h"
#include "config.h"

static sqlite3* db = NULL;

void
db_create(void)
{
	const char* dbpath = expand_tilde(DB_PATH);

	const char* create_table = "CREATE TABLE IF NOT EXISTS posts ("
				   "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
				   "  title TEXT NOT NULL,"
				   "  link TEXT NOT NULL,"
				   "  feed_url TEXT NOT NULL,"
				   "  comments TEXT NULL,"
				   "  pub_date TEXT,"
				   "  summary TEXT,"
				   "  seen INTEGER DEFAULT 0,"
				   "  UNIQUE(feed_url, link)"
				   ");";

	if (sqlite3_open(dbpath, &db) != SQLITE_OK) {
		fprintf(stderr, "could not open/create db file at path %s\n", dbpath);
		exit(1);
	}

	if (sqlite3_exec(db, create_table, NULL, NULL, NULL) != SQLITE_OK) {
		fprintf(stderr, "could not create table in db at path %s\n", DB_PATH);
		exit(1);
	}
}

void
db_insert_post(db_post_t dbp)
{
	if (db == NULL) {
		fprintf(stderr, "fetch_posts: bad args / missing db\n");
		exit(1);
	}
	if (dbp.link == NULL) {
		fprintf(stderr, "trying to insert a dbpost without a link!\n");
		exit(1);
	} else if (dbp.title == NULL) {
		fprintf(stderr, "trying to insert a dbpost without a title.\n");
		exit(1);
	} else if (dbp.feed_url == NULL) {
		fprintf(stderr, "trying to insert a dbpost without a feed.\n");
		exit(1);
	}

	sqlite3_stmt* ins = NULL;
	const char* ins_sql = "INSERT INTO posts(title, link, feed_url, comments, pub_date, summary, seen)"
			      " VALUES(?, ?, ?, ?, ?, ?, ?) ON CONFLICT(feed_url, link) DO NOTHING;";

	int rc = sqlite3_prepare_v2(db, ins_sql, -1, &ins, NULL);
	if (rc != SQLITE_OK) {
		fprintf(stderr, "prepare failed: %s\n", sqlite3_errmsg(db));
		exit(1);
	}

	sqlite3_reset(ins);

	sqlite3_clear_bindings(ins);

	if (sqlite3_bind_text(ins, 1, dbp.title, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: title");

	if (sqlite3_bind_text(ins, 2, dbp.link, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: link");

	if (sqlite3_bind_text(ins, 3, dbp.feed_url, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: feed_url");

	if (sqlite3_bind_text(ins, 4, dbp.comments ? dbp.comments : NULL, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: comments");

	if (sqlite3_bind_text(ins, 5, dbp.pub_date ? dbp.pub_date : NULL, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: pub_date");

	if (sqlite3_bind_text(ins, 6, dbp.summary ? dbp.summary : NULL, -1, SQLITE_TRANSIENT) != SQLITE_OK)
		perror("failed db insertion of: summary");

	if (sqlite3_bind_int(ins, 7, dbp.seen ? dbp.seen : 0) != SQLITE_OK)
		perror("failed db insertion of: seen");

	rc = sqlite3_step(ins);
	if (rc != SQLITE_DONE) {
		printf("%s\n", dbp.link);
		fprintf(stderr, "step failed: %s\n", sqlite3_errmsg(db));
	} else {
		int changes = sqlite3_changes(db);
		if (changes == 0) {
			// fprintf(stderr, "no change\n");
		} else {
			// sqlite3_int64 rowid = sqlite3_last_insert_rowid(db);
			// fprintf(stderr, "inserted rowid = %lld\n", (long long)rowid);
		}
	}

	sqlite3_finalize(ins);
}

static char*
dup_col(sqlite3_stmt* st, int i)
{
	const unsigned char* t = sqlite3_column_text(st, i);
	if (!t) return NULL;
	size_t n = strlen((const char*)t);
	char* s = (char*)ecalloc(n + 1, sizeof(char));
	if (!s) return NULL;
	memcpy(s, t, n + 1);
	return s;
}

db_fetch_post_t
db_fetch_posts(const char* feed_url)
{
	if (!db || !feed_url) {
		fprintf(stderr, "fetch_posts: bad args / missing db\n");
		return (db_fetch_post_t) { .success = 0 };
	}

	const char* sql = "SELECT id, title, link, feed_url, comments, pub_date, summary, seen "
			  "FROM posts WHERE feed_url = ? "
			  "ORDER BY id DESC LIMIT ?;";

	sqlite3_stmt* st = NULL;
	if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
		fprintf(stderr, "prepare failed: %s\n", sqlite3_errmsg(db));
		return (db_fetch_post_t) { .success = 0 };
	}
	sqlite3_bind_text(st, 1, feed_url, -1, SQLITE_TRANSIENT);
	sqlite3_bind_int(st, 2, MAX_POST_PER_FEED);

	int len = 0, cap = 0;
	db_post_t** rows = NULL;

	while (sqlite3_step(st) == SQLITE_ROW) {
		db_post_t* p = ecalloc(1, sizeof(db_post_t));
		p->id = sqlite3_column_int(st, 0);
		p->title = dup_col(st, 1);
		p->link = dup_col(st, 2);
		p->feed_url = dup_col(st, 3);
		p->comments = dup_col(st, 4);
		p->pub_date = dup_col(st, 5);
		p->summary = dup_col(st, 6);
		p->seen = sqlite3_column_int(st, 7);
		if (len == cap) {
			cap = cap ? cap * 2 : MAX_POST_PER_FEED;
			rows = (db_post_t**)realloc(rows, sizeof(db_post_t*) * (size_t)cap);
			if (rows == NULL) {
				fprintf(stderr, "could not reallocate for list of posts fetch from db\n");
				break;
			}
		}
		rows[len++] = p;
	}

	int rc = sqlite3_finalize(st);

	if (rc != SQLITE_OK) {
		fprintf(stderr, "finalize: %s\n", sqlite3_errmsg(db));
		for (int i = 0; i < len; i++) {
			free(rows[i]->title);
			free(rows[i]->link);
			free(rows[i]->feed_url);
			free(rows[i]->comments);
			free(rows[i]->pub_date);
			free(rows[i]->summary);
		}
		free(rows);
		return (db_fetch_post_t) { .success = 0 };
	}

	return (db_fetch_post_t) {
		.posts = rows,
		.count = len,
		.success = 1,
	};
}

int
db_mark_as_seen(int id)
{
	if (!db) {
		fprintf(stderr, "db_mark_as_seen: missing db\n");
		return 1;
	}

	const char* sql = "UPDATE posts SET seen=1 WHERE id=?;";
	sqlite3_stmt* st = NULL;

	if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
		fprintf(stderr, "db_mark_as_seen: prepare: %s\n", sqlite3_errmsg(db));
		return 1;
	}
	if (sqlite3_bind_int(st, 1, id) != SQLITE_OK) {
		fprintf(stderr, "db_mark_as_seen: bind id: %s\n", sqlite3_errmsg(db));
		sqlite3_finalize(st);
		return 1;
	}

	int rc = sqlite3_step(st);
	if (rc != SQLITE_DONE) {
		fprintf(stderr, "db_mark_as_seen: step: %s\n", sqlite3_errmsg(db));
		sqlite3_finalize(st);
		return 1;
	}

	sqlite3_finalize(st);
	return 0;
}

void
db_close(void)
{
	if (sqlite3_close(db) != SQLITE_OK) {
		fprintf(stderr, "prepare insert failed\n");
		exit(1);
	}
}