docs: add rustdoc comments
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
//! Event persistence and querying
|
||||
use crate::error::Result;
|
||||
use crate::event::Event;
|
||||
use crate::subscription::Subscription;
|
||||
@@ -9,10 +10,12 @@ use rusqlite::OpenFlags;
|
||||
use std::path::Path;
|
||||
use tokio::task;
|
||||
|
||||
/// Database file
|
||||
const DB_FILE: &str = "nostr.db";
|
||||
|
||||
// schema
|
||||
/// Schema definition
|
||||
const INIT_SQL: &str = r##"
|
||||
-- Database settings
|
||||
PRAGMA encoding = "UTF-8";
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA main.synchronous=NORMAL;
|
||||
@@ -20,6 +23,8 @@ PRAGMA foreign_keys = ON;
|
||||
PRAGMA application_id = 1654008667;
|
||||
PRAGMA user_version = 1;
|
||||
pragma mmap_size = 536870912; -- 512MB of mmap
|
||||
|
||||
-- Event Table
|
||||
CREATE TABLE IF NOT EXISTS event (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_hash BLOB NOT NULL, -- 4-byte hash
|
||||
@@ -29,23 +34,33 @@ author BLOB NOT NULL, -- author pubkey
|
||||
kind INTEGER NOT NULL, -- event kind
|
||||
content TEXT NOT NULL -- serialized json of event object
|
||||
);
|
||||
|
||||
-- Event Indexes
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
|
||||
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
|
||||
CREATE INDEX IF NOT EXISTS author_index ON event(author);
|
||||
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
|
||||
|
||||
-- Event References Table
|
||||
CREATE TABLE IF NOT EXISTS event_ref (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_id INTEGER NOT NULL, -- an event ID that contains an #e tag.
|
||||
referenced_event BLOB NOT NULL, -- the event that is referenced.
|
||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Event References Index
|
||||
CREATE INDEX IF NOT EXISTS event_ref_index ON event_ref(referenced_event);
|
||||
|
||||
-- Pubkey References Table
|
||||
CREATE TABLE IF NOT EXISTS pubkey_ref (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_id INTEGER NOT NULL, -- an event ID that contains an #p tag.
|
||||
referenced_pubkey BLOB NOT NULL, -- the pubkey that is referenced.
|
||||
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE RESTRICT ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Pubkey References Index
|
||||
CREATE INDEX IF NOT EXISTS pubkey_ref_index ON pubkey_ref(referenced_pubkey);
|
||||
"##;
|
||||
|
||||
@@ -70,7 +85,6 @@ pub async fn db_writer(
|
||||
let next_event = event_rx.blocking_recv();
|
||||
// if the channel has closed, we will never get work
|
||||
if next_event.is_none() {
|
||||
info!("No more event senders for DB, shutting down.");
|
||||
break;
|
||||
}
|
||||
let event = next_event.unwrap();
|
||||
@@ -84,7 +98,7 @@ pub async fn db_writer(
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
info!("event insert failed: {}", err);
|
||||
warn!("event insert failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +108,7 @@ pub async fn db_writer(
|
||||
})
|
||||
}
|
||||
|
||||
/// Persist an event to the database.
|
||||
pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
||||
// start transaction
|
||||
let tx = conn.transaction()?;
|
||||
@@ -101,15 +116,21 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
||||
let id_blob = hex::decode(&e.id).ok();
|
||||
let pubkey_blob = hex::decode(&e.pubkey).ok();
|
||||
let event_str = serde_json::to_string(&e).ok();
|
||||
// ignore if the event hash is a duplicate.x
|
||||
// ignore if the event hash is a duplicate.
|
||||
let ins_count = tx.execute(
|
||||
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'));",
|
||||
params![id_blob, e.created_at, e.kind, pubkey_blob, event_str]
|
||||
)?;
|
||||
if ins_count == 0 {
|
||||
// if the event was a duplicate, no need to insert event or
|
||||
// pubkey references.
|
||||
return Ok(ins_count);
|
||||
}
|
||||
// remember primary key of the event most recently inserted.
|
||||
let ev_id = tx.last_insert_rowid();
|
||||
// add all event tags into the event_ref table
|
||||
let etags = e.get_event_tags();
|
||||
if etags.len() > 0 {
|
||||
// this will need to
|
||||
for etag in etags.iter() {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO event_ref (event_id, referenced_event) VALUES (?1, ?2)",
|
||||
@@ -117,6 +138,7 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
||||
)?;
|
||||
}
|
||||
}
|
||||
// add all event tags into the pubkey_ref table
|
||||
let ptags = e.get_pubkey_tags();
|
||||
if ptags.len() > 0 {
|
||||
for ptag in ptags.iter() {
|
||||
@@ -130,18 +152,21 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
|
||||
Ok(ins_count)
|
||||
}
|
||||
|
||||
// Queries return a subscription identifier and the serialized event.
|
||||
/// Event resulting from a specific subscription request
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct QueryResult {
|
||||
/// Subscription identifier
|
||||
pub sub_id: String,
|
||||
/// Serialized event
|
||||
pub event: String,
|
||||
}
|
||||
|
||||
// TODO: make this hex
|
||||
fn is_alphanum(s: &str) -> bool {
|
||||
/// Check if a string contains only hex characters.
|
||||
fn is_hex(s: &str) -> bool {
|
||||
s.chars().all(|x| char::is_ascii_hexdigit(&x))
|
||||
}
|
||||
|
||||
/// Create a dynamic SQL query string from a subscription.
|
||||
fn query_from_sub(sub: &Subscription) -> String {
|
||||
// build a dynamic SQL query. all user-input is either an integer
|
||||
// (sqli-safe), or a string that is filtered to only contain
|
||||
@@ -150,7 +175,6 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN event_ref er ON e.id=er.event_id LEFT JOIN pubkey_ref pr ON e.id=pr.event_id "
|
||||
.to_owned();
|
||||
// for every filter in the subscription, generate a where clause
|
||||
// all individual filter clause strings for this subscription
|
||||
let mut filter_clauses: Vec<String> = Vec::new();
|
||||
for f in sub.filters.iter() {
|
||||
// individual filter components
|
||||
@@ -160,7 +184,7 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
// I believe the author & authors fields are redundant.
|
||||
if f.author.is_some() {
|
||||
let author_str = f.author.as_ref().unwrap();
|
||||
if is_alphanum(author_str) {
|
||||
if is_hex(author_str) {
|
||||
let author_clause = format!("author = x'{}'", author_str);
|
||||
filter_components.push(author_clause);
|
||||
}
|
||||
@@ -172,7 +196,7 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|&x| is_alphanum(x))
|
||||
.filter(|&x| is_hex(x))
|
||||
.map(|x| format!("x'{}'", x))
|
||||
.collect();
|
||||
let authors_clause = format!("author IN ({})", authors_escaped.join(", "));
|
||||
@@ -186,34 +210,30 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
}
|
||||
// Query for event
|
||||
if f.id.is_some() {
|
||||
// whitelist characters
|
||||
let id_str = f.id.as_ref().unwrap();
|
||||
if is_alphanum(id_str) {
|
||||
if is_hex(id_str) {
|
||||
let id_clause = format!("event_hash = x'{}'", id_str);
|
||||
filter_components.push(id_clause);
|
||||
}
|
||||
}
|
||||
// Query for referenced event
|
||||
if f.event.is_some() {
|
||||
// whitelist characters
|
||||
let ev_str = f.event.as_ref().unwrap();
|
||||
if is_alphanum(ev_str) {
|
||||
if is_hex(ev_str) {
|
||||
let ev_clause = format!("referenced_event = x'{}'", ev_str);
|
||||
filter_components.push(ev_clause);
|
||||
}
|
||||
}
|
||||
// Query for referenced pet name pubkey
|
||||
if f.pubkey.is_some() {
|
||||
// whitelist characters
|
||||
let pet_str = f.pubkey.as_ref().unwrap();
|
||||
if is_alphanum(pet_str) {
|
||||
if is_hex(pet_str) {
|
||||
let pet_clause = format!("referenced_pubkey = x'{}'", pet_str);
|
||||
filter_components.push(pet_clause);
|
||||
}
|
||||
}
|
||||
// Query for timestamp
|
||||
if f.since.is_some() {
|
||||
// timestamp is number, no escaping needed
|
||||
let created_clause = format!("created_at > {}", f.since.unwrap());
|
||||
filter_components.push(created_clause);
|
||||
}
|
||||
@@ -231,10 +251,16 @@ fn query_from_sub(sub: &Subscription) -> String {
|
||||
query.push_str(" WHERE ");
|
||||
query.push_str(&filter_clauses.join(" OR "));
|
||||
}
|
||||
info!("Query: {}", query);
|
||||
return query;
|
||||
debug!("Query: {}", query);
|
||||
query
|
||||
}
|
||||
|
||||
/// Perform a database query using a subscription.
|
||||
///
|
||||
/// The [`Subscription`] is converted into a SQL query. Each result
|
||||
/// is published on the `query_tx` channel as it is returned. If a
|
||||
/// message becomes available on the `abandon_query_rx` channel, the
|
||||
/// query is immediately aborted.
|
||||
pub async fn db_query(
|
||||
sub: Subscription,
|
||||
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
|
||||
@@ -246,22 +272,19 @@ pub async fn db_query(
|
||||
.unwrap();
|
||||
info!("Opened database for reading");
|
||||
info!("Going to query for: {:?}", sub);
|
||||
// generate query
|
||||
// generate SQL query
|
||||
let q = query_from_sub(&sub);
|
||||
|
||||
// execute the query
|
||||
let mut stmt = conn.prepare(&q).unwrap();
|
||||
let mut event_rows = stmt.query([]).unwrap();
|
||||
let mut i: usize = 0;
|
||||
while let Some(row) = event_rows.next().unwrap() {
|
||||
// check if this is still active (we could do this every N rows)
|
||||
if abandon_query_rx.try_recv().is_ok() {
|
||||
info!("Abandoning query...");
|
||||
// we have received a request to abandon the query
|
||||
debug!("query aborted");
|
||||
return;
|
||||
}
|
||||
// TODO: check before unwrapping
|
||||
let event_json = row.get(0).unwrap();
|
||||
i += 1;
|
||||
info!("Sending event #{}", i);
|
||||
query_tx
|
||||
.blocking_send(QueryResult {
|
||||
sub_id: sub.get_id(),
|
||||
@@ -269,6 +292,6 @@ pub async fn db_query(
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
info!("Finished reading");
|
||||
debug!("query completed");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user