-
-
Notifications
You must be signed in to change notification settings - Fork 138
Nostr sqldb #835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Nostr sqldb #835
Changes from 5 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
dc668e3
Init crate
tompro 2a9fa03
Merge branch 'rust-nostr:master' into master
tompro 2dd01b4
Added example
tompro cd82800
nostr-postgresdb formatting
tompro 0846696
Deleted filters
tompro 88d7f89
Rename crate
tompro dbf7e73
Formatting
tompro 21819f8
Multi db features
tompro 68ed5b3
Generic schema and migrations, separate postgres
tompro 0bbb01d
Cargo name
tompro 81009ce
Fix example
tompro bd86247
Reuse FlatBufferBuilder
tompro 84d1bfb
Remove signature, use binary columns
tompro fbc48db
Merge branch 'master' into nostr-postgresdb
tompro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[package] | ||
name = "nostr-postgresdb" | ||
version = "0.41.0" | ||
edition = "2021" | ||
description = "Postgres storage backend for Nostr apps" | ||
authors.workspace = true | ||
homepage.workspace = true | ||
repository.workspace = true | ||
license.workspace = true | ||
readme = "README.md" | ||
rust-version.workspace = true | ||
keywords = ["nostr", "database", "postgres"] | ||
|
||
[dependencies] | ||
nostr = { workspace = true, features = ["std"] } | ||
nostr-database = { workspace = true, features = ["flatbuf"] } | ||
tracing.workspace = true | ||
diesel = { version = "2", features = ["postgres", "serde_json"] } | ||
diesel-async = { version = "0.5", features = ["postgres", "deadpool"] } | ||
diesel_migrations = { version = "2", features = ["postgres"] } | ||
deadpool = { version = "0.12", features = ["managed", "rt_tokio_1"] } | ||
|
||
[dev-dependencies] | ||
tokio.workspace = true | ||
nostr-relay-builder = { workspace = true } | ||
tracing-subscriber = { workspace = true } | ||
|
||
[[example]] | ||
name = "postgres-relay" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Nostr Postgres database backend | ||
|
||
Postgres storage backend for nostr apps | ||
|
||
## State | ||
|
||
**This library is in an ALPHA state**, things that are implemented generally | ||
work but the API will change in breaking ways. | ||
|
||
## Donations | ||
|
||
`rust-nostr` is free and open-source. This means we do not earn any revenue by | ||
selling it. Instead, we rely on your financial support. If you actively use any | ||
of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). | ||
|
||
## License | ||
|
||
This project is distributed under the MIT software license - see the | ||
[LICENSE](../../LICENSE) file for details |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# For documentation on how to configure this file, | ||
# see https://diesel.rs/guides/configuring-diesel-cli | ||
|
||
[print_schema] | ||
file = "src/schema.rs" | ||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] | ||
schema = "nostr" | ||
|
||
[migrations_directory] | ||
dir = "migrations" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright (c) 2025 Protom | ||
// Distributed under the MIT software license | ||
|
||
use std::time::Duration; | ||
|
||
use nostr_database::prelude::*; | ||
use nostr_postgresdb::NostrPostgres; | ||
use nostr_relay_builder::prelude::*; | ||
|
||
// Your database URL | ||
const DB_URL: &str = "postgres://postgres:password@localhost:5432"; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<()> { | ||
tracing_subscriber::fmt::init(); | ||
|
||
// This will programatically run pending db migrations | ||
nostr_postgresdb::run_migrations(DB_URL)?; | ||
|
||
// Create a conncetion pool | ||
let pool = nostr_postgresdb::postgres_connection_pool(DB_URL).await?; | ||
|
||
// Create a nostr db instance | ||
let db: NostrPostgres = pool.into(); | ||
|
||
// Add db to builder | ||
let builder = RelayBuilder::default().database(db); | ||
|
||
// Create local relay | ||
let relay = LocalRelay::run(builder).await?; | ||
println!("Url: {}", relay.url()); | ||
|
||
// Keep up the program | ||
loop { | ||
tokio::time::sleep(Duration::from_secs(60)).await; | ||
} | ||
} |
Empty file.
6 changes: 6 additions & 0 deletions
6
crates/nostr-postgresdb/migrations/00000000000000_diesel_initial_setup/down.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
-- This file was automatically created by Diesel to setup helper functions | ||
-- and other internal bookkeeping. This file is safe to edit, any future | ||
-- changes will be added to existing projects as new migrations. | ||
|
||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); | ||
DROP FUNCTION IF EXISTS diesel_set_updated_at(); |
36 changes: 36 additions & 0 deletions
36
crates/nostr-postgresdb/migrations/00000000000000_diesel_initial_setup/up.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
-- This file was automatically created by Diesel to setup helper functions | ||
-- and other internal bookkeeping. This file is safe to edit, any future | ||
-- changes will be added to existing projects as new migrations. | ||
|
||
|
||
|
||
|
||
-- Sets up a trigger for the given table to automatically set a column called | ||
-- `updated_at` whenever the row is modified (unless `updated_at` was included | ||
-- in the modified columns) | ||
-- | ||
-- # Example | ||
-- | ||
-- ```sql | ||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); | ||
-- | ||
-- SELECT diesel_manage_updated_at('users'); | ||
-- ``` | ||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ | ||
BEGIN | ||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s | ||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); | ||
END; | ||
$$ LANGUAGE plpgsql; | ||
|
||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ | ||
BEGIN | ||
IF ( | ||
NEW IS DISTINCT FROM OLD AND | ||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at | ||
) THEN | ||
NEW.updated_at := current_timestamp; | ||
END IF; | ||
RETURN NEW; | ||
END; | ||
$$ LANGUAGE plpgsql; |
4 changes: 4 additions & 0 deletions
4
crates/nostr-postgresdb/migrations/2025-04-11-095120_events/down.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
-- This file should undo anything in `up.sql` | ||
DROP TABLE nostr.event_tags; | ||
DROP TABLE nostr.events; | ||
DROP SCHEMA nostr; |
30 changes: 30 additions & 0 deletions
30
crates/nostr-postgresdb/migrations/2025-04-11-095120_events/up.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
-- Init the schema | ||
CREATE SCHEMA IF NOT EXISTS nostr; | ||
|
||
-- The actual event data | ||
CREATE TABLE nostr.events ( | ||
id VARCHAR(64) PRIMARY KEY, | ||
pubkey VARCHAR(64) NOT NULL, | ||
created_at BIGINT NOT NULL, | ||
kind BIGINT NOT NULL, | ||
payload BYTEA NOT NULL, | ||
signature VARCHAR(128) NOT NULL, | ||
deleted BOOLEAN NOT NULL | ||
); | ||
|
||
-- Direct indexes | ||
CREATE INDEX event_pubkey ON nostr.events (pubkey); | ||
CREATE INDEX event_date ON nostr.events (created_at); | ||
CREATE INDEX event_kind ON nostr.events (kind); | ||
CREATE INDEX event_deleted ON nostr.events (deleted); | ||
|
||
-- The tag index, the primary will give us the index automatically | ||
CREATE TABLE nostr.event_tags ( | ||
tag TEXT NOT NULL, | ||
tag_value TEXT NOT NULL, | ||
event_id VARCHAR(64) NOT NULL | ||
REFERENCES nostr.events (id) | ||
ON DELETE CASCADE | ||
ON UPDATE CASCADE, | ||
PRIMARY KEY (tag, tag_value, event_id) | ||
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
mod migrations; | ||
mod model; | ||
mod postgres; | ||
mod schema; | ||
|
||
use diesel::prelude::*; | ||
use diesel_async::RunQueryDsl; | ||
pub use migrations::run_migrations; | ||
use model::{EventDataDb, EventDb}; | ||
use nostr::event::*; | ||
use nostr::filter::Filter; | ||
use nostr::types::Timestamp; | ||
use nostr::util::BoxedFuture; | ||
use nostr_database::*; | ||
use postgres::{build_filter_query, with_limit}; | ||
pub use postgres::{postgres_connection_pool, NostrPostgres}; | ||
use schema::nostr::events; | ||
|
||
impl NostrDatabase for NostrPostgres { | ||
fn backend(&self) -> Backend { | ||
Backend::Custom("Postgres".to_string()) | ||
} | ||
} | ||
|
||
impl NostrEventsDatabase for NostrPostgres { | ||
/// Save [`Event`] into store | ||
/// | ||
/// **This method assumes that [`Event`] was already verified** | ||
fn save_event<'a>( | ||
&'a self, | ||
event: &'a Event, | ||
) -> BoxedFuture<'a, Result<SaveEventStatus, DatabaseError>> { | ||
Box::pin(async move { self.save(EventDataDb::try_from(event)?).await }) | ||
} | ||
|
||
/// Check event status by ID | ||
/// | ||
/// Check if the event is saved, deleted or not existent. | ||
fn check_id<'a>( | ||
&'a self, | ||
event_id: &'a EventId, | ||
) -> BoxedFuture<'a, Result<DatabaseEventStatus, DatabaseError>> { | ||
Box::pin(async move { | ||
let status = match self.event_by_id(event_id).await? { | ||
Some(e) if e.deleted => DatabaseEventStatus::Deleted, | ||
Some(_) => DatabaseEventStatus::Saved, | ||
None => DatabaseEventStatus::NotExistent, | ||
}; | ||
Ok(status) | ||
}) | ||
} | ||
|
||
/// Coordinate feature is not supported yet | ||
fn has_coordinate_been_deleted<'a>( | ||
&'a self, | ||
_coordinate: &'a nostr::nips::nip01::CoordinateBorrow<'a>, | ||
_timestamp: &'a Timestamp, | ||
) -> BoxedFuture<'a, Result<bool, DatabaseError>> { | ||
Box::pin(async move { Ok(false) }) | ||
} | ||
|
||
/// Get [`Event`] by [`EventId`] | ||
fn event_by_id<'a>( | ||
&'a self, | ||
_event_id: &'a EventId, | ||
) -> BoxedFuture<'a, Result<Option<Event>, DatabaseError>> { | ||
Box::pin(async move { | ||
let event = match self.event_by_id(_event_id).await? { | ||
Some(e) if !e.deleted => { | ||
Some(Event::decode(&e.payload).map_err(DatabaseError::backend)?) | ||
} | ||
_ => None, | ||
}; | ||
Ok(event) | ||
}) | ||
} | ||
|
||
/// Count the number of events found with [`Filter`]. | ||
/// | ||
/// Use `Filter::new()` or `Filter::default()` to count all events. | ||
fn count(&self, filter: Filter) -> BoxedFuture<Result<usize, DatabaseError>> { | ||
Box::pin(async move { | ||
let res: i64 = build_filter_query(filter) | ||
.count() | ||
.get_result(&mut self.get_connection().await?) | ||
.await | ||
.map_err(DatabaseError::backend)?; | ||
Ok(res as usize) | ||
}) | ||
} | ||
|
||
/// Query stored events. | ||
fn query(&self, filter: Filter) -> BoxedFuture<Result<Events, DatabaseError>> { | ||
let filter = with_limit(filter, 10000); | ||
Box::pin(async move { | ||
let mut events = Events::new(&filter); | ||
let result = build_filter_query(filter.clone()) | ||
.select(EventDb::as_select()) | ||
.load(&mut self.get_connection().await?) | ||
.await | ||
.map_err(DatabaseError::backend)?; | ||
|
||
for item in result.into_iter() { | ||
if let Ok(event) = Event::decode(&item.payload) { | ||
events.insert(event); | ||
} | ||
} | ||
Ok(events) | ||
}) | ||
} | ||
|
||
/// Delete all events that match the [Filter] | ||
fn delete(&self, filter: Filter) -> BoxedFuture<Result<(), DatabaseError>> { | ||
let filter = with_limit(filter, 999); | ||
Box::pin(async move { | ||
let filter = build_filter_query(filter); | ||
diesel::update(events::table) | ||
.set(events::deleted.eq(true)) | ||
.filter(events::id.eq_any(filter.select(events::id))) | ||
.execute(&mut self.get_connection().await?) | ||
.await | ||
.map_err(DatabaseError::backend)?; | ||
|
||
Ok(()) | ||
}) | ||
} | ||
} | ||
|
||
/// For now we want to avoid wiping the database | ||
impl NostrDatabaseWipe for NostrPostgres { | ||
#[inline] | ||
fn wipe(&self) -> BoxedFuture<Result<(), DatabaseError>> { | ||
Box::pin(async move { Err(DatabaseError::NotSupported) }) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
use diesel::{Connection, PgConnection}; | ||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; | ||
use nostr_database::DatabaseError; | ||
use tracing::info; | ||
|
||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); | ||
|
||
/// programatically run the db migrations | ||
pub fn run_migrations(connection_string: &str) -> Result<(), DatabaseError> { | ||
info!("Running db migrations in postgres database",); | ||
let mut connection = | ||
PgConnection::establish(connection_string).map_err(DatabaseError::backend)?; | ||
|
||
let res = connection | ||
.run_pending_migrations(MIGRATIONS) | ||
.map_err(DatabaseError::Backend)?; | ||
info!("Successfully executed postgres db migrations {:?}", res); | ||
Ok(()) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.