diff --git a/Cargo.lock b/Cargo.lock index 4c73c8088..e484d6487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "amplify" version = "4.6.1" @@ -209,7 +215,7 @@ dependencies = [ "rand", "safelog", "serde", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-chanmgr", @@ -253,7 +259,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.8", + "thiserror 2.0.17", ] [[package]] @@ -366,6 +372,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -868,6 +883,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1316,6 +1346,12 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "2.0.1" @@ -1394,6 +1430,9 @@ name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -1462,6 +1501,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -1598,6 +1648,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1619,7 +1675,7 @@ dependencies = [ "once_cell", "pwd-grp", "serde", - "thiserror 2.0.8", + "thiserror 2.0.17", "walkdir", ] @@ -1650,7 +1706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f261f25f1e94963fe8f72863f4da841b280fa3b5a573990b425a26b585a54578" dependencies = [ "fslock-arti-fork", - "thiserror 2.0.8", + "thiserror 2.0.17", "winapi", ] @@ -1702,6 +1758,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1864,6 +1931,17 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -1879,6 +1957,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "heck" version = "0.4.1" @@ -2008,12 +2095,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -2082,16 +2169,15 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 0.26.6", + "webpki-roots", ] [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ - "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2099,9 +2185,7 @@ dependencies = [ "http", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -2236,33 +2320,12 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.0", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.13" @@ -2509,9 +2572,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" [[package]] name = "macroific" @@ -2569,6 +2632,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -2921,6 +2994,20 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "nostr-sqldb" +version = "0.44.0" +dependencies = [ + "nostr", + "nostr-database", + "nostr-database-test-suite", + "nostr-relay-builder", + "sqlx", + "tempfile", + "tokio", + "tracing-subscriber", +] + [[package]] name = "nostrdb" version = "0.8.0" @@ -2932,7 +3019,7 @@ dependencies = [ "flatbuffers 23.5.26", "futures", "libc", - "thiserror 2.0.8", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -3644,7 +3731,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.8", + "thiserror 2.0.17", ] [[package]] @@ -3693,9 +3780,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "base64 0.22.1", "bytes", @@ -3707,13 +3794,17 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", + "ipnet", "js-sys", "log", + "mime", "mime_guess", + "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -3721,14 +3812,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower", - "tower-http", + "tokio-socks", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.1", + "webpki-roots", + "windows-registry", ] [[package]] @@ -3806,7 +3897,7 @@ dependencies = [ "bitflags 2.9.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink", + "hashlink 0.9.1", "libsqlite3-sys", "smallvec", "time", @@ -3876,9 +3967,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -3889,17 +3980,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -3928,7 +4032,7 @@ dependencies = [ "educe", "either", "fluid-let", - "thiserror 2.0.8", + "thiserror 2.0.17", ] [[package]] @@ -4112,9 +4216,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -4290,7 +4394,7 @@ dependencies = [ "paste", "serde", "slotmap", - "thiserror 2.0.8", + "thiserror 2.0.17", "void", ] @@ -4299,6 +4403,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -4335,6 +4442,196 @@ dependencies = [ "der", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.2", + "hashlink 0.10.0", + "indexmap 2.12.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.90", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.90", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "tracing", + "url", +] + [[package]] name = "ssh-cipher" version = "0.2.0" @@ -4382,6 +4679,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.10.0" @@ -4512,11 +4820,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.8" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.8", + "thiserror-impl 2.0.17", ] [[package]] @@ -4532,9 +4840,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.8" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -4608,19 +4916,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -4660,6 +4966,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.26.1" @@ -4673,7 +4990,7 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots 0.26.6", + "webpki-roots", ] [[package]] @@ -4736,7 +5053,7 @@ dependencies = [ "oneshot-fused-workaround", "pin-project", "postage", - "thiserror 2.0.8", + "thiserror 2.0.17", "void", ] @@ -4756,7 +5073,7 @@ dependencies = [ "serde", "slab", "smallvec", - "thiserror 2.0.8", + "thiserror 2.0.17", ] [[package]] @@ -4771,7 +5088,7 @@ dependencies = [ "educe", "getrandom 0.2.15", "safelog", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-error", "tor-llcrypto", "zeroize", @@ -4793,7 +5110,7 @@ dependencies = [ "paste", "rand", "smallvec", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-cert", @@ -4816,7 +5133,7 @@ dependencies = [ "derive_builder_fork_arti", "derive_more 2.0.1", "digest", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-bytes", "tor-checkable", "tor-llcrypto", @@ -4839,7 +5156,7 @@ dependencies = [ "rand", "safelog", "serde", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-cell", @@ -4865,7 +5182,7 @@ checksum = "8f1671c146d35ead4a350a50d7d2b25230635c0271539d310d92ea8d7c777313" dependencies = [ "humantime", "signature", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-llcrypto", ] @@ -4895,7 +5212,7 @@ dependencies = [ "safelog", "serde", "static_assertions", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-chanmgr", @@ -4942,7 +5259,7 @@ dependencies = [ "serde-value", "serde_ignored", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "toml", "tor-basic-utils", "tor-error", @@ -4961,7 +5278,7 @@ dependencies = [ "once_cell", "serde", "shellexpand", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-error", "tor-general-addr", ] @@ -4974,7 +5291,7 @@ checksum = "9b9c48e1e8cc9c925ae5bdca8c71952886d2407f1f286cc4d8f4f7aad082d6a6" dependencies = [ "digest", "hex", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-llcrypto", ] @@ -4994,7 +5311,7 @@ dependencies = [ "httpdate", "itertools 0.14.0", "memchr", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-circmgr", "tor-error", "tor-hscrypto", @@ -5038,7 +5355,7 @@ dependencies = [ "serde", "signature", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "time", "tor-async-utils", "tor-basic-utils", @@ -5071,7 +5388,7 @@ dependencies = [ "retry-error", "static_assertions", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tracing", "void", ] @@ -5083,7 +5400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f35f8ecb457f99f655c805f6c5cc855c63e71fa84c24a48e11e9fc51a7d7ad4b" dependencies = [ "derive_more 2.0.1", - "thiserror 2.0.8", + "thiserror 2.0.17", "void", ] @@ -5112,7 +5429,7 @@ dependencies = [ "safelog", "serde", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-config", @@ -5149,7 +5466,7 @@ dependencies = [ "safelog", "slotmap-careful", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-bytes", @@ -5189,7 +5506,7 @@ dependencies = [ "safelog", "signature", "subtle", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-error", @@ -5215,7 +5532,7 @@ dependencies = [ "safelog", "serde", "serde_with", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-cell", "tor-config", @@ -5260,7 +5577,7 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-bytes", @@ -5299,7 +5616,7 @@ dependencies = [ "rand", "signature", "ssh-key", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-bytes", "tor-cert", "tor-checkable", @@ -5330,7 +5647,7 @@ dependencies = [ "serde", "signature", "ssh-key", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-config", @@ -5363,7 +5680,7 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-config", @@ -5399,7 +5716,7 @@ dependencies = [ "sha3", "signature", "subtle", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-memquota", "visibility", "x25519-dalek", @@ -5415,7 +5732,7 @@ dependencies = [ "futures", "humantime", "once_cell", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-error", "tor-rtcompat", "tracing", @@ -5439,7 +5756,7 @@ dependencies = [ "serde", "slotmap-careful", "static_assertions", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-config", @@ -5469,7 +5786,7 @@ dependencies = [ "serde", "static_assertions", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "time", "tor-basic-utils", "tor-error", @@ -5508,7 +5825,7 @@ dependencies = [ "signature", "smallvec", "subtle", - "thiserror 2.0.8", + "thiserror 2.0.17", "time", "tinystr", "tor-basic-utils", @@ -5547,7 +5864,7 @@ dependencies = [ "sanitize-filename", "serde", "serde_json", - "thiserror 2.0.8", + "thiserror 2.0.17", "time", "tor-async-utils", "tor-basic-utils", @@ -5586,7 +5903,7 @@ dependencies = [ "slotmap-careful", "static_assertions", "subtle", - "thiserror 2.0.8", + "thiserror 2.0.17", "tokio", "tokio-util", "tor-async-utils", @@ -5619,7 +5936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a7d228eda4c7e7c96fff6a5f6759d1bd03bad69b62b9d94f2ac409de3518b8a" dependencies = [ "caret", - "thiserror 2.0.8", + "thiserror 2.0.17", ] [[package]] @@ -5655,7 +5972,7 @@ dependencies = [ "paste", "pin-project", "rustls-pki-types", - "thiserror 2.0.8", + "thiserror 2.0.17", "tokio", "tokio-util", "tor-error", @@ -5684,7 +6001,7 @@ dependencies = [ "priority-queue", "slotmap-careful", "strum", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-error", "tor-general-addr", "tor-rtcompat", @@ -5705,7 +6022,7 @@ dependencies = [ "educe", "safelog", "subtle", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-bytes", "tor-error", ] @@ -5718,49 +6035,10 @@ checksum = "7388f506c9278d07421e6799aa8a912adee4ea6921b3dd08a1247a619de82124" dependencies = [ "derive-deftly 1.0.1", "derive_more 2.0.1", - "thiserror 2.0.8", + "thiserror 2.0.17", "tor-memquota", ] -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -5773,6 +6051,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5871,7 +6150,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.8", + "thiserror 2.0.17", "utf-8", ] @@ -5923,6 +6202,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -5976,7 +6261,7 @@ dependencies = [ "rustls", "rustls-pki-types", "url", - "webpki-roots 0.26.6", + "webpki-roots", ] [[package]] @@ -6017,9 +6302,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.1", "js-sys", @@ -6096,6 +6381,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasix" version = "0.12.21" @@ -6133,13 +6424,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", "web-sys", ] @@ -6218,15 +6508,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "4.4.2" @@ -6239,6 +6520,16 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6279,6 +6570,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index e1fc20cb7..b5ae16fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "database/nostr-indexeddb", "database/nostr-lmdb", "database/nostr-ndb", + "database/nostr-sqldb", # Gossip "gossip/nostr-gossip", @@ -51,6 +52,7 @@ nostr-gossip-memory = { version = "0.44", path = "./gossip/nostr-gossip-memory", nostr-gossip-test-suite = { path = "./gossip/nostr-gossip-test-suite" } nostr-lmdb = { version = "0.44", path = "./database/nostr-lmdb", default-features = false } nostr-ndb = { version = "0.44", path = "./database/nostr-ndb", default-features = false } +nostr-sqldb = { version = "0.44", path = "./database/nostr-sqldb", default-features = false } nostr-relay-builder = { version = "0.44", path = "./crates/nostr-relay-builder", default-features = false } nostr-relay-pool = { version = "0.44", path = "./crates/nostr-relay-pool", default-features = false } reqwest = { version = "0.12", default-features = false } diff --git a/README.md b/README.md index 784399459..86fd856f4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The project is split up into several crates: - [**nostr-database**](./database/nostr-database): Events database abstraction and in-memory implementation - [**nostr-lmdb**](./database/nostr-lmdb): LMDB storage backend - [**nostr-ndb**](./database/nostr-ndb): [nostrdb](https://github.com/damus-io/nostrdb) storage backend + - [**nostr-sqldb**](./database/nostr-sqldb): SQL storage backends (PostgreSQL, MySQL and SQLite) - [**nostr-indexeddb**](./database/nostr-indexeddb): IndexedDB storage backend - [**nostr-gossip**](./gossip/nostr-gossip): Gossip traits - [**nostr-gossip-memory**](./gossip/nostr-gossip-memory): In-memory gossip database diff --git a/contrib/scripts/check-crates.sh b/contrib/scripts/check-crates.sh index 5cf7b2578..a06e3bae2 100755 --- a/contrib/scripts/check-crates.sh +++ b/contrib/scripts/check-crates.sh @@ -39,6 +39,9 @@ buildargs=( "-p nostr-gossip-memory" "-p nostr-gossip-test-suite" "-p nostr-lmdb" + "-p nostr-sqldb --no-default-features --features postgres" # PostgreSQL + "-p nostr-sqldb --no-default-features --features mysql" # MySQL + "-p nostr-sqldb --no-default-features --features sqlite" # SQLite "-p nostr-indexeddb --target wasm32-unknown-unknown" "-p nostr-ndb" "-p nostr-keyring" @@ -55,6 +58,9 @@ buildargs=( skip_msrv=( "-p nostr-lmdb" # MSRV: 1.72.0 + "-p nostr-sqldb --no-default-features --features postgres" # MSRV: 1.82.0 + "-p nostr-sqldb --no-default-features --features mysql" # MSRV: 1.82.0 + "-p nostr-sqldb --no-default-features --features sqlite" # MSRV: 1.82.0 "-p nostr-keyring" # MSRV: 1.75.0 "-p nostr-keyring --features async" # MSRV: 1.75.0 "-p nostr-sdk --features tor" # MSRV: 1.77.0 diff --git a/crates/nostr-sdk/examples/client.rs b/crates/nostr-sdk/examples/client.rs index a93406b9e..1add5a827 100644 --- a/crates/nostr-sdk/examples/client.rs +++ b/crates/nostr-sdk/examples/client.rs @@ -2,6 +2,8 @@ // Copyright (c) 2023-2025 Rust Nostr Developers // Distributed under the MIT software license +use std::time::Duration; + use nostr_sdk::prelude::*; #[tokio::main] @@ -11,9 +13,7 @@ async fn main() -> Result<()> { let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; let client = Client::new(keys); - client.add_relay("wss://relay.damus.io").await?; - client.add_relay("wss://nostr.wine").await?; - client.add_relay("wss://relay.rip").await?; + client.add_relay("ws://127.0.0.1:17445").await?; client.connect().await; @@ -24,15 +24,13 @@ async fn main() -> Result<()> { println!("Sent to: {:?}", output.success); println!("Not sent to: {:?}", output.failed); - // Create a text note POW event to relays - let builder = EventBuilder::text_note("POW text note from rust-nostr").pow(20); - client.send_event_builder(builder).await?; - - // Send a text note POW event to specific relays - let builder = EventBuilder::text_note("POW text note from rust-nostr 16").pow(16); - client - .send_event_builder_to(["wss://relay.damus.io", "wss://relay.rip"], builder) + let events = client + .fetch_events(Filter::new().kind(Kind::TextNote), Duration::from_secs(10)) .await?; + for event in events { + println!("{}", event.as_json()) + } + Ok(()) } diff --git a/database/nostr-sqldb/Cargo.toml b/database/nostr-sqldb/Cargo.toml new file mode 100644 index 000000000..473d9d42a --- /dev/null +++ b/database/nostr-sqldb/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "nostr-sqldb" +version = "0.44.0" +edition = "2021" +description = "SQL 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", "mysql", "sqlite"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = ["sqlite"] +# Enable SQLite support +sqlite = ["sqlx/sqlite"] +# Enable Postgres support +postgres = ["sqlx/postgres", "sqlx/tls-rustls-ring-webpki"] +# Enable MySQL/MariaDB support +mysql = ["sqlx/mysql", "sqlx/tls-rustls-ring-webpki"] + +[dependencies] +nostr = { workspace = true, features = ["std"] } +nostr-database = { workspace = true, features = ["flatbuf"] } +sqlx = { version = "0.8", features = ["migrate", "runtime-tokio"] } +tokio = { workspace = true, features = ["sync"] } + +[dev-dependencies] +nostr-database-test-suite.workspace = true +nostr-relay-builder.workspace = true +tempfile.workspace = true +tokio.workspace = true +tracing-subscriber.workspace = true + +[[example]] +name = "postgres-relay" +required-features = ["postgres"] + +[[example]] +name = "sqlite-relay" +required-features = ["sqlite"] diff --git a/database/nostr-sqldb/README.md b/database/nostr-sqldb/README.md new file mode 100644 index 000000000..b2641b6a1 --- /dev/null +++ b/database/nostr-sqldb/README.md @@ -0,0 +1,25 @@ +# Nostr SQL database backend + +SQL storage backend for nostr apps working with Postgres, SQLite and MySQL. + +## Crate Feature Flags + +The following crate feature flags are available: + +| Feature | Default | Description | +|-------------|:-------:|-------------------------------| +| `postgres` | Yes | Enable support for PostgreSQL | +| `mysql` | No | Enable support for MySQL | +| `sqlite` | No | Enable support for SQLite | + +## 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 diff --git a/database/nostr-sqldb/build.rs b/database/nostr-sqldb/build.rs new file mode 100644 index 000000000..4ca5b40bf --- /dev/null +++ b/database/nostr-sqldb/build.rs @@ -0,0 +1,7 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +fn main() { + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/database/nostr-sqldb/examples/postgres-relay.rs b/database/nostr-sqldb/examples/postgres-relay.rs new file mode 100644 index 000000000..0ad3f94c6 --- /dev/null +++ b/database/nostr-sqldb/examples/postgres-relay.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +use std::time::Duration; + +use nostr_database::prelude::*; +use nostr_relay_builder::prelude::*; +use nostr_sqldb::{NostrSql, NostrSqlBackend}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let backend = NostrSqlBackend::Postgres { + host: String::from("localhost"), + port: 5432, + username: Some(String::from("postgres")), + password: Some(String::from("password")), + database: String::from("nostr"), + }; + + // Create a nostr db instance and run pending db migrations if any + let db = NostrSql::new(backend).await?; + + // 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; + } +} diff --git a/database/nostr-sqldb/examples/sqlite-relay.rs b/database/nostr-sqldb/examples/sqlite-relay.rs new file mode 100644 index 000000000..0ad681c73 --- /dev/null +++ b/database/nostr-sqldb/examples/sqlite-relay.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +use std::time::Duration; + +use nostr_database::prelude::*; +use nostr_relay_builder::prelude::*; +use nostr_sqldb::{NostrSql, NostrSqlBackend}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let backend = NostrSqlBackend::sqlite("nostr.db"); + + // Create a nostr db instance and run pending db migrations if any + let db = NostrSql::new(backend).await?; + + // Add db to builder + let builder = RelayBuilder::default().database(db); + + // Create local relay + let relay = LocalRelay::new(builder); + relay.run().await?; + println!("Url: {}", relay.url().await); + + // Keep up the program + loop { + tokio::time::sleep(Duration::from_secs(60)).await; + } +} diff --git a/database/nostr-sqldb/migrations/mysql/001_init.sql b/database/nostr-sqldb/migrations/mysql/001_init.sql new file mode 100644 index 000000000..b59b514d2 --- /dev/null +++ b/database/nostr-sqldb/migrations/mysql/001_init.sql @@ -0,0 +1,26 @@ +-- The actual event data +CREATE TABLE events ( + id BLOB(32) PRIMARY KEY NOT NULL, + pubkey BLOB(32) NOT NULL, + created_at BIGINT NOT NULL, + kind BIGINT NOT NULL, + payload BLOB NOT NULL, + deleted BOOLEAN NOT NULL +); + +-- Direct indexes +CREATE INDEX event_pubkey ON events (pubkey); +CREATE INDEX event_date ON events (created_at); +CREATE INDEX event_kind ON events (kind); +CREATE INDEX event_deleted ON events (deleted); + +-- The tag index, the primary will give us the index automatically +CREATE TABLE event_tags ( + tag VARCHAR(64) NOT NULL, + tag_value VARCHAR(512) NOT NULL, + event_id BLOB(32) NOT NULL + REFERENCES events (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + PRIMARY KEY (tag, tag_value, event_id) +); diff --git a/database/nostr-sqldb/migrations/postgres/001_init.sql b/database/nostr-sqldb/migrations/postgres/001_init.sql new file mode 100644 index 000000000..74f300294 --- /dev/null +++ b/database/nostr-sqldb/migrations/postgres/001_init.sql @@ -0,0 +1,26 @@ +-- The actual event data +CREATE TABLE events ( + id BYTEA PRIMARY KEY NOT NULL, + pubkey BYTEA NOT NULL, + created_at BIGINT NOT NULL, + kind BIGINT NOT NULL, + payload BYTEA NOT NULL, + deleted BOOLEAN NOT NULL +); + +-- Direct indexes +CREATE INDEX event_pubkey ON events (pubkey); +CREATE INDEX event_date ON events (created_at); +CREATE INDEX event_kind ON events (kind); +CREATE INDEX event_deleted ON events (deleted); + +-- The tag index, the primary will give us the index automatically +CREATE TABLE event_tags ( + tag TEXT NOT NULL, + tag_value TEXT NOT NULL, + event_id BYTEA NOT NULL + REFERENCES events (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + PRIMARY KEY (tag, tag_value, event_id) +); diff --git a/database/nostr-sqldb/migrations/sqlite/001_init.sql b/database/nostr-sqldb/migrations/sqlite/001_init.sql new file mode 100644 index 000000000..8d1f54314 --- /dev/null +++ b/database/nostr-sqldb/migrations/sqlite/001_init.sql @@ -0,0 +1,26 @@ +-- The actual event data +CREATE TABLE events ( + id BLOB PRIMARY KEY NOT NULL, + pubkey BLOB NOT NULL, + created_at BIGINT NOT NULL, + kind BIGINT NOT NULL, + payload BLOB NOT NULL, + deleted BOOLEAN NOT NULL +); + +-- Direct indexes +CREATE INDEX event_pubkey ON events (pubkey); +CREATE INDEX event_date ON events (created_at); +CREATE INDEX event_kind ON events (kind); +CREATE INDEX event_deleted ON events (deleted); + +-- The tag index, the primary will give us the index automatically +CREATE TABLE event_tags ( + tag TEXT NOT NULL, + tag_value TEXT NOT NULL, + event_id BLOB NOT NULL + REFERENCES events (id) + ON DELETE CASCADE + ON UPDATE CASCADE, + PRIMARY KEY (tag, tag_value, event_id) +); diff --git a/database/nostr-sqldb/src/db.rs b/database/nostr-sqldb/src/db.rs new file mode 100644 index 000000000..ab3c021f2 --- /dev/null +++ b/database/nostr-sqldb/src/db.rs @@ -0,0 +1,510 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +//! Nostr SQL + +use std::fmt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use nostr_database::prelude::*; +use sqlx::migrate::Migrator; +#[cfg(feature = "mysql")] +use sqlx::mysql::MySqlConnectOptions; +#[cfg(feature = "postgres")] +use sqlx::postgres::PgConnectOptions; +#[cfg(feature = "sqlite")] +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::{ + Any, AnyConnection, AnyPool, ConnectOptions, Database, Pool, QueryBuilder, Sqlite, Transaction, + Type, +}; +use tokio::sync::Mutex; + +use crate::error::Error; +use crate::model::{EventDataDb, EventDb, EventTagDb}; + +const EVENTS_QUERY_LIMIT: usize = 10_000; + +/// SQL backend +pub enum NostrSqlBackend { + /// SQLite + #[cfg(feature = "sqlite")] + Sqlite { + /// SQLite database path + /// + /// If no path is passed, an in-memory database will be created. + path: Option, + }, + /// Postgres + #[cfg(feature = "postgres")] + Postgres { + /// Host + host: String, + /// Port + port: u16, + /// Username + username: Option, + /// Password + password: Option, + /// Database name + database: String, + }, +} + +impl NostrSqlBackend { + /// New persistent SQLite database + #[inline] + #[cfg(feature = "sqlite")] + pub fn sqlite

(path: P) -> Self + where + P: AsRef, + { + Self::Sqlite { + path: Some(path.as_ref().to_path_buf()), + } + } + + /// New in-memory SQLite database + #[inline] + #[cfg(feature = "sqlite")] + pub fn sqlite_memory() -> Self { + Self::Sqlite { path: None } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum PoolKind { + #[cfg(feature = "sqlite")] + Sqlite, + #[cfg(feature = "postgres")] + Postgres, + #[cfg(feature = "mysql")] + MySql, +} + +/// Nostr SQL database +#[derive(Clone)] +pub struct NostrSql { + pool: AnyPool, + kind: PoolKind, + fbb: Arc>>, +} + +impl fmt::Debug for NostrSql { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NostrSql") + .field("pool", &self.pool) + .finish() + } +} + +impl NostrSql { + /// Connect to a SQL database + pub async fn new(backend: NostrSqlBackend) -> Result { + // Install drivers + sqlx::any::install_default_drivers(); + + let (uri, kind) = match backend { + #[cfg(feature = "sqlite")] + NostrSqlBackend::Sqlite { path } => { + let mut opts: SqliteConnectOptions = + SqliteConnectOptions::new().create_if_missing(true); + + match path { + Some(path) => opts = opts.filename(path), + None => opts = opts.in_memory(true), + }; + + (opts.to_url_lossy(), PoolKind::Sqlite) + } + #[cfg(feature = "postgres")] + NostrSqlBackend::Postgres { + host, + port, + username, + password, + database, + } => { + let mut opts: PgConnectOptions = PgConnectOptions::new_without_pgpass() + .host(&host) + .port(port) + .database(&database); + + if let Some(username) = username { + opts = opts.username(&username); + } + + if let Some(password) = password { + opts = opts.password(&password); + } + + (opts.to_url_lossy(), PoolKind::Postgres) + } + }; + + let pool: AnyPool = AnyPool::connect(uri.as_str()).await?; + + let migrator: Migrator = match kind { + #[cfg(feature = "sqlite")] + PoolKind::Sqlite => sqlx::migrate!("migrations/sqlite"), + #[cfg(feature = "postgres")] + PoolKind::Postgres => sqlx::migrate!("migrations/postgres"), + #[cfg(feature = "mysql")] + PoolKind::MySql => sqlx::migrate!("migrations/mysql"), + }; + migrator.run(&pool).await?; + + Ok(Self { + pool, + kind, + fbb: Arc::new(Mutex::new(FlatBufferBuilder::new())), + }) + } + + /// Returns true if successfully inserted + async fn insert_event_tx( + &self, + tx: &mut Transaction<'_, Any>, + event: &EventDb, + ) -> Result { + let sql: &str = match self.kind { + #[cfg(feature = "sqlite")] + PoolKind::Sqlite => { + "INSERT OR IGNORE INTO events (id, pubkey, created_at, kind, payload, deleted) VALUES (?, ?, ?, ?, ?, ?)" + }, + #[cfg(feature = "postgres")] + PoolKind::Postgres => { + "INSERT INTO events (id, pubkey, created_at, kind, payload, deleted) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO NOTHING" + }, + #[cfg(feature = "mysql")] + PoolKind::MySql => { + "INSERT IGNORE INTO events (id, pubkey, created_at, kind, payload, deleted) VALUES (?, ?, ?, ?, ?, ?)" + }, + }; + + let result = sqlx::query(sql) + .bind(&event.id) + .bind(&event.pubkey) + .bind(event.created_at) + .bind(event.kind) + .bind(&event.payload) + .bind(event.deleted) + .execute(&mut **tx) + .await?; + + Ok(result.rows_affected() > 0) + } + + async fn insert_tags_tx( + &self, + tx: &mut Transaction<'_, Any>, + tags: &[EventTagDb], + ) -> Result<(), Error> { + let sql: &str = match self.kind { + #[cfg(feature = "sqlite")] + PoolKind::Sqlite => { + "INSERT OR IGNORE INTO event_tags (tag, tag_value, event_id) VALUES (?, ?, ?)" + }, + #[cfg(feature = "postgres")] + PoolKind::Postgres => { + "INSERT INTO event_tags (tag, tag_value, event_id) VALUES (?, ?, ?) ON CONFLICT (tag, tag_value, event_id) DO NOTHING" + }, + #[cfg(feature = "mysql")] + PoolKind::MySql => { + "INSERT IGNORE INTO event_tags (tag, tag_value, event_id) VALUES (?, ?, ?)" + }, + }; + + for tag in tags { + sqlx::query(sql) + .bind(&tag.tag) + .bind(&tag.tag_value) + .bind(&tag.event_id) + .execute(&mut **tx) + .await?; + } + + Ok(()) + } + + async fn _save_event(&self, event: &Event) -> Result { + if event.kind.is_ephemeral() { + return Ok(SaveEventStatus::Rejected(RejectedReason::Ephemeral)); + } + + let mut tx = self.pool.begin().await?; + + // Convert event + let data: EventDataDb = { + let mut fbb = self.fbb.lock().await; + EventDataDb::from_event(event, &mut fbb) + }; + + // TODO: check if event is deleted + // TODO: check if is replaced + + // Insert event first + let inserted: bool = self.insert_event_tx(&mut tx, &data.event).await?; + + // Check if the event has been inserted + if inserted { + // Insert tags + if !data.tags.is_empty() { + self.insert_tags_tx(&mut tx, &data.tags).await?; + } + + // Commit transaction + tx.commit().await?; + + Ok(SaveEventStatus::Success) + } else { + // Event has not been inserted, rollback transaction + tx.rollback().await?; + Ok(SaveEventStatus::Rejected(RejectedReason::Duplicate)) + } + } + + async fn get_event_by_id(&self, id: &EventId) -> Result, Error> { + let event: Option = sqlx::query_as( + "SELECT id, pubkey, created_at, kind, payload, deleted FROM events WHERE id = ?", + ) + .bind(id.as_bytes().to_vec()) + .fetch_optional(&self.pool) + .await?; + Ok(event) + } +} + +impl NostrDatabase for NostrSql { + fn backend(&self) -> Backend { + match self.kind { + #[cfg(feature = "sqlite")] + PoolKind::Sqlite => Backend::SQLite, + #[cfg(feature = "postgres")] + PoolKind::Postgres => Backend::Custom(String::from("Postgres")), + #[cfg(feature = "mysql")] + PoolKind::MySql => Backend::Custom(String::from("MySQL")), + } + } + + fn save_event<'a>( + &'a self, + event: &'a Event, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + self._save_event(event) + .await + .map_err(DatabaseError::backend) + }) + } + + fn check_id<'a>( + &'a self, + event_id: &'a EventId, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + match self + .get_event_by_id(event_id) + .await + .map_err(DatabaseError::backend)? + { + Some(e) if e.is_deleted() => Ok(DatabaseEventStatus::Deleted), + Some(_) => Ok(DatabaseEventStatus::Saved), + None => Ok(DatabaseEventStatus::NotExistent), + } + }) + } + + fn event_by_id<'a>( + &'a self, + event_id: &'a EventId, + ) -> BoxedFuture<'a, Result, DatabaseError>> { + Box::pin(async move { + match self + .get_event_by_id(event_id) + .await + .map_err(DatabaseError::backend)? + { + Some(e) if !e.is_deleted() => Ok(Some( + Event::decode(&e.payload).map_err(DatabaseError::backend)?, + )), + _ => Ok(None), + } + }) + } + + fn count(&self, filter: Filter) -> BoxedFuture> { + Box::pin(async move { Ok(self.query(filter).await?.len()) }) + } + + fn query(&self, filter: Filter) -> BoxedFuture> { + Box::pin(async move { + // Limit filter query + let filter: Filter = with_limit(filter, EVENTS_QUERY_LIMIT); + + let mut events: Events = Events::new(&filter); + + let sql = build_filter_query(filter); + + let payloads: Vec<(Vec,)> = sqlx::query_as(&sql) + .fetch_all(&self.pool) + .await + .map_err(DatabaseError::backend)?; + + for (payload,) in payloads.into_iter() { + if let Ok(event) = Event::decode(&payload) { + events.insert(event); + } + } + Ok(events) + }) + } + + fn delete(&self, _filter: Filter) -> BoxedFuture> { + // Box::pin(async move { + // let filter = with_limit(filter, 999); + // 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(()) + // }) + Box::pin(async move { Err(DatabaseError::NotSupported) }) + } + + fn wipe(&self) -> BoxedFuture> { + Box::pin(async move { Err(DatabaseError::NotSupported) }) + } +} + +fn build_filter_query(filter: Filter) -> String { + let mut query_builder: QueryBuilder = QueryBuilder::new( + "SELECT DISTINCT e.payload + FROM events e + INNER JOIN event_tags et ON e.id = et.event_id + WHERE e.deleted = 0", + ); + + // Add filters + if let Some(ids) = filter.ids { + if !ids.is_empty() { + query_builder.push(" AND e.id IN ("); + let mut separated = query_builder.separated(", "); + for id in ids.into_iter() { + separated.push_bind(id.as_bytes().to_vec()); + } + query_builder.push(")"); + } + } + + if let Some(authors) = filter.authors { + if !authors.is_empty() { + query_builder.push(" AND e.pubkey IN ("); + let mut separated = query_builder.separated(", "); + for author in authors { + separated.push_bind(author.as_bytes().to_vec()); + } + query_builder.push(")"); + } + } + + if let Some(kinds) = filter.kinds { + if !kinds.is_empty() { + query_builder.push(" AND e.kind IN ("); + let mut separated = query_builder.separated(", "); + for kind in kinds { + separated.push_bind(kind.as_u16() as i64); + } + query_builder.push(")"); + } + } + + if let Some(since) = filter.since { + query_builder.push(" AND e.created_at >= "); + query_builder.push_bind(since.as_secs() as i64); + } + + if let Some(until) = filter.until { + query_builder.push(" AND e.created_at <= "); + query_builder.push_bind(until.as_secs() as i64); + } + + if !filter.generic_tags.is_empty() { + for (tag, values) in filter.generic_tags { + if !values.is_empty() { + query_builder.push( + " AND EXISTS ( + SELECT 1 FROM event_tags et2 + WHERE et2.event_id = e.id + AND et2.tag = ", + ); + query_builder.push_bind(tag.to_string()); + query_builder.push(" AND et2.tag_value IN ("); + + let mut separated = query_builder.separated(", "); + for value in values { + separated.push_bind(value.to_string()); + } + query_builder.push("))"); + } + } + } + + query_builder.push(" ORDER BY e.created_at DESC"); + + if let Some(limit) = filter.limit { + query_builder.push(" LIMIT "); + query_builder.push_bind(limit as i64); + } + + query_builder.into_sql() +} + +/// sets the given default limit on a Nostr filter if not set +fn with_limit(filter: Filter, default_limit: usize) -> Filter { + if filter.limit.is_none() { + return filter.limit(default_limit); + } + filter +} + +#[cfg(test)] +mod tests { + use nostr_database_test_suite::database_unit_tests; + use tempfile::TempDir; + + use super::*; + + struct TempDatabase { + db: NostrSql, + // Needed to avoid the drop and deletion of temp folder + _temp: TempDir, + } + + impl Deref for TempDatabase { + type Target = NostrSql; + + fn deref(&self) -> &Self::Target { + &self.db + } + } + + impl TempDatabase { + async fn new() -> Self { + let path = tempfile::tempdir().unwrap(); + let backend = NostrSqlBackend::sqlite(path.path().join("test.db")); + Self { + db: NostrSql::new(backend).await.unwrap(), + _temp: path, + } + } + } + + database_unit_tests!(TempDatabase, TempDatabase::new); +} diff --git a/database/nostr-sqldb/src/error.rs b/database/nostr-sqldb/src/error.rs new file mode 100644 index 000000000..8a6f6b2f2 --- /dev/null +++ b/database/nostr-sqldb/src/error.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +//! Error + +use std::fmt; + +use sqlx::migrate::MigrateError; + +/// Nostr SQL error +#[derive(Debug)] +pub enum Error { + /// SQLx error + Sqlx(sqlx::Error), + /// Migration error + Migrate(MigrateError), +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sqlx(e) => write!(f, "{e}"), + Self::Migrate(e) => write!(f, "{e}"), + } + } +} + +impl From for Error { + fn from(e: sqlx::Error) -> Self { + Self::Sqlx(e) + } +} + +impl From for Error { + fn from(e: MigrateError) -> Self { + Self::Migrate(e) + } +} diff --git a/database/nostr-sqldb/src/lib.rs b/database/nostr-sqldb/src/lib.rs new file mode 100644 index 000000000..0a4610689 --- /dev/null +++ b/database/nostr-sqldb/src/lib.rs @@ -0,0 +1,22 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +//! Nostr SQL database + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] + +#[cfg(not(any(feature = "sqlite", feature = "postgres", feature = "mysql")))] +compile_error!("At least one database backend must be enabled"); + +pub mod db; +pub mod error; +mod model; +// #[cfg(feature = "sqlite")] +// pub mod sqlite; + +pub use self::db::{NostrSql, NostrSqlBackend}; diff --git a/database/nostr-sqldb/src/model.rs b/database/nostr-sqldb/src/model.rs new file mode 100644 index 000000000..7f5cbcd52 --- /dev/null +++ b/database/nostr-sqldb/src/model.rs @@ -0,0 +1,72 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +use nostr::event::Event; +use nostr_database::{FlatBufferBuilder, FlatBufferEncode}; +use sqlx::FromRow; + +#[derive(Debug, Clone, FromRow)] +pub(crate) struct EventDb { + pub id: Vec, + pub pubkey: Vec, + pub created_at: i64, + pub kind: i64, + pub payload: Vec, + pub deleted: bool, +} + +impl EventDb { + #[inline] + pub(super) fn is_deleted(&self) -> bool { + self.deleted + } +} + +#[derive(Debug, Clone, FromRow)] +pub(crate) struct EventTagDb { + pub tag: String, + pub tag_value: String, + pub event_id: Vec, +} + +/// A data container for extracting data from [`Event`] and its tags +#[derive(Debug, Clone)] +pub(crate) struct EventDataDb { + pub event: EventDb, + pub tags: Vec, +} + +impl EventDataDb { + pub(crate) fn from_event(event: &Event, fbb: &mut FlatBufferBuilder) -> Self { + Self { + event: EventDb { + id: event.id.as_bytes().to_vec(), + pubkey: event.pubkey.as_bytes().to_vec(), + created_at: event.created_at.as_secs() as i64, + kind: event.kind.as_u16() as i64, + payload: event.encode(fbb).to_vec(), + deleted: false, + }, + tags: extract_tags(event), + } + } +} + +fn extract_tags(event: &Event) -> Vec { + event + .tags + .iter() + .filter_map(|tag| { + if let (kind, Some(content)) = (tag.kind(), tag.content()) { + Some(EventTagDb { + tag: kind.to_string(), + tag_value: content.to_string(), + event_id: event.id.as_bytes().to_vec(), + }) + } else { + None + } + }) + .collect() +} diff --git a/database/nostr-sqldb/src/sqlite.rs b/database/nostr-sqldb/src/sqlite.rs new file mode 100644 index 000000000..7dee4258e --- /dev/null +++ b/database/nostr-sqldb/src/sqlite.rs @@ -0,0 +1,109 @@ +use std::path::Path; +use std::sync::Arc; + +use sqlx::{Sqlite, SqlitePool}; +use sqlx::migrate::Migrator; +use sqlx::sqlite::SqliteConnectOptions; +use tokio::sync::Mutex; +use nostr::{Event, EventId, Filter}; +use nostr::prelude::BoxedFuture; +use nostr_database::{Backend, DatabaseError, DatabaseEventStatus, Events, FlatBufferBuilder, NostrDatabase, SaveEventStatus}; + +use crate::db::NostrSql; +use crate::error::Error; + +#[derive(Debug, Clone)] +pub struct NostrSqlite { + db: NostrSql +} + +impl NostrSqlite { + /// Open SQLite database + pub async fn open

(path: P) -> Result + where + P: AsRef, + { + // Build SQLite connection options + let opts: SqliteConnectOptions = + SqliteConnectOptions::new().create_if_missing(true).filename(path); + + // Connect to SQLite database + let pool: SqlitePool = SqlitePool::connect_with(opts).await?; + + // Run migrations + let migrator: Migrator = sqlx::migrate!("migrations/sqlite"); + migrator.run(&pool).await?; + + Ok(Self { + db: NostrSql::new(pool), + }) + } +} + +impl NostrDatabase for NostrSqlite { + fn backend(&self) -> Backend { + self.db.backend() + } + + fn save_event<'a>(&'a self, event: &'a Event) -> BoxedFuture<'a, Result> { + self.db.save_event(event) + } + + fn check_id<'a>(&'a self, event_id: &'a EventId) -> BoxedFuture<'a, Result> { + self.db.check_id(event_id) + } + + fn event_by_id<'a>(&'a self, event_id: &'a EventId) -> BoxedFuture<'a, Result, DatabaseError>> { + self.db.event_by_id(event_id) + } + + fn count(&self, filter: Filter) -> BoxedFuture> { + self.db.count(filter) + } + + fn query(&self, filter: Filter) -> BoxedFuture> { + self.db.query(filter) + } + + fn delete(&self, filter: Filter) -> BoxedFuture> { + self.db.delete(filter) + } + + fn wipe(&self) -> BoxedFuture> { + self.db.wipe() + } +} + +#[cfg(test)] +mod tests { + use nostr_database_test_suite::database_unit_tests; + use tempfile::TempDir; + + use super::*; + + struct TempDatabase { + db: NostrSqlite, + // Needed to avoid the drop and deletion of temp folder + _temp: TempDir, + } + + impl Deref for TempDatabase { + type Target = NostrSqlite; + + fn deref(&self) -> &Self::Target { + &self.db + } + } + + impl TempDatabase { + async fn new() -> Self { + let path = tempfile::tempdir().unwrap(); + Self { + db: NostrSqlite::open(path.path().join("temp.db")).await.unwrap(), + _temp: path, + } + } + } + + database_unit_tests!(TempDatabase, TempDatabase::new); +}