Compare commits
2 commits
70cdfe1941
...
85473b4938
Author | SHA1 | Date | |
---|---|---|---|
|
85473b4938 | ||
|
a71420fc7b |
8 changed files with 57 additions and 204 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -1868,6 +1868,7 @@ dependencies = [
|
|||
"time",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
|
@ -2228,15 +2229,6 @@ version = "1.16.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
|
||||
[[package]]
|
||||
name = "ulid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
|
@ -2308,6 +2300,16 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -2454,8 +2456,8 @@ dependencies = [
|
|||
"tower-http 0.4.1",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ulid",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -22,7 +22,8 @@ justerror = "1"
|
|||
password-hash = { version = "0.5", features = ["std", "getrandom"] }
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sqlx = { version = "0.6", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "chrono", "time"] }
|
||||
sqlx = { version = "0.6", default-features = false, features = ["runtime-tokio-rustls", "any",
|
||||
"sqlite", "chrono", "time", "uuid"] }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["full", "tracing"], default-features = false }
|
||||
tokio-retry = "0.3.0"
|
||||
|
@ -31,9 +32,9 @@ tower = { version = "0.4", features = ["util", "timeout"], default-features = fa
|
|||
tower-http = { version = "0.4", features = ["add-extension", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
ulid = { version = "1", features = ["rand"] }
|
||||
unicode-segmentation = "1"
|
||||
rand_distr = "0.4.3"
|
||||
uuid = { version = "1.4.0", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "9.0.0"
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
-- indices
|
||||
drop index if exists user_username_dex;
|
||||
drop index if exists user_email_dex;
|
||||
drop index if exists watch_title_dex;
|
||||
drop index if exists witch_added_by_dex;
|
||||
drop index if exists quests_user_dex;
|
||||
drop index if exists quests_watch_dex;
|
||||
drop index if exists note_user_dex;
|
||||
drop index if exists note_watch_dex;
|
||||
drop index if exists user_dex;
|
||||
drop index if exists watch_dex;
|
||||
drop index if exists w2wdex;
|
||||
drop index if exists note_dex;
|
||||
-- tables
|
||||
drop table if exists watch_quests;
|
||||
drop table if exists watch_quest;
|
||||
drop table if exists watch_notes;
|
||||
drop table if exists follows;
|
||||
drop table if exists users;
|
||||
|
|
|
@ -29,24 +29,25 @@ create table if not exists watches (
|
|||
|
||||
-- table of what people want to watch
|
||||
create table if not exists watch_quests (
|
||||
id blob not null primary key,
|
||||
user blob not null,
|
||||
watch blob not null,
|
||||
party blob, -- list of user IDs, but we can also scan for friends that want to watch the same thing
|
||||
priority int, -- 1-5 how much do you want to watch it
|
||||
public boolean not null default true,
|
||||
watched boolean not null default false,
|
||||
when_added int not null default (unixepoch()),
|
||||
public boolean not null,
|
||||
watched boolean not null,
|
||||
when_added int,
|
||||
when_watched int,
|
||||
last_updated int not null default (unixepoch()),
|
||||
foreign key (user) references users (id) on delete cascade on update no action,
|
||||
foreign key (watch) references watches (id) on delete cascade on update no action,
|
||||
primary key (user, watch)
|
||||
) without rowid;
|
||||
foreign key (watch) references watches (id) on delete cascade on update no action
|
||||
);
|
||||
|
||||
-- friend lists; this should really be a graph db, maybe the whole thing should be
|
||||
-- TODO: look into replacing sqlite with https://www.cozodb.org/
|
||||
create table if not exists follows (
|
||||
user blob not null primary key,
|
||||
follows blob, -- possibly empty friends list in some app-specific format
|
||||
coven blob, -- possibly empty friends list in some app-specific format
|
||||
last_updated int not null default (unixepoch()),
|
||||
foreign key (user) references users (id) on delete cascade on update no action
|
||||
);
|
||||
|
@ -64,10 +65,10 @@ create table if not exists watch_notes (
|
|||
|
||||
-- indices, not needed for follows
|
||||
create index if not exists user_username_dex on users (username);
|
||||
create index if not exists user_email_dex on users (lower(email));
|
||||
create index if not exists watch_title_dex on watches (lower(title));
|
||||
create index if not exists user_email_dex on users (email);
|
||||
create index if not exists watch_title_dex on watches (title);
|
||||
create index if not exists watch_added_by_dex on watches (added_by);
|
||||
create index if not exists quests_user_dex on watch_quests (user);
|
||||
create index if not exists quests_watch_dex on watch_quests (watch);
|
||||
create index if not exists questswatch_dex on watch_quests (watch);
|
||||
create index if not exists note_user_dex on watch_notes (user);
|
||||
create index if not exists note_watch_dex on watch_notes (watch);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
drop trigger if exists update_last_updated_users;
|
||||
drop trigger if exists update_last_updated_watches;
|
||||
drop trigger if exists update_last_updated_watch_quests;
|
||||
drop trigger if exists update_last_updated_watch_quest;
|
||||
drop trigger if exists update_last_updated_follows;
|
||||
drop trigger if exists update_last_updated_watch_notes;
|
||||
|
|
|
@ -16,7 +16,7 @@ create trigger if not exists update_last_updated_watch_quests
|
|||
after update on watch_quests
|
||||
when OLD.last_updated = NEW.last_updated or OLD.last_updated is null
|
||||
BEGIN
|
||||
update watch_quests set last_updated = (select unixepoch()) where id=NEW.id;
|
||||
update watch_quest set last_updated = (select unixepoch()) where id=NEW.id;
|
||||
END;
|
||||
|
||||
create trigger if not exists update_last_updated_follows
|
||||
|
|
185
src/db_id.rs
185
src/db_id.rs
|
@ -1,48 +1,34 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Debug, Display},
|
||||
};
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use chrono::Utc;
|
||||
use serde::{de::Visitor, Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull,
|
||||
sqlite::{SqliteArgumentValue, SqliteValueRef},
|
||||
Decode, Encode, Sqlite,
|
||||
};
|
||||
use ulid::Ulid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::{Error, Uuid};
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DbId(pub Ulid);
|
||||
#[derive(
|
||||
Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, Deserialize,
|
||||
)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct DbId(pub Uuid);
|
||||
|
||||
impl DbId {
|
||||
pub fn bytes(&self) -> [u8; 16] {
|
||||
self.to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn to_be_bytes(self) -> [u8; 16] {
|
||||
self.0 .0.to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn is_nil(&self) -> bool {
|
||||
self.0.is_nil()
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self(Ulid::new())
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_string(s: &str) -> Result<Self, ulid::DecodeError> {
|
||||
let id = Ulid::from_string(s)?;
|
||||
Ok(id.into())
|
||||
pub fn from_string(s: &str) -> Result<Self, Error> {
|
||||
Ok(Self(Uuid::try_parse(s)?))
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
self.0.simple().to_string()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> chrono::DateTime<Utc> {
|
||||
self.0.datetime().into()
|
||||
chrono::DateTime::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,153 +48,14 @@ impl Debug for DbId {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Ulid> for DbId {
|
||||
fn from(value: Ulid) -> Self {
|
||||
impl From<Uuid> for DbId {
|
||||
fn from(value: Uuid) -> Self {
|
||||
DbId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for DbId {
|
||||
fn from(value: u128) -> Self {
|
||||
DbId(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
//-************************************************************************
|
||||
// sqlx traits for going in and out of the db
|
||||
//-************************************************************************
|
||||
|
||||
impl sqlx::Type<sqlx::Sqlite> for DbId {
|
||||
fn type_info() -> <sqlx::Sqlite as sqlx::Database>::TypeInfo {
|
||||
<&[u8] as sqlx::Type<sqlx::Sqlite>>::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Encode<'q, Sqlite> for DbId {
|
||||
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
|
||||
args.push(SqliteArgumentValue::Blob(Cow::Owned(self.bytes().to_vec())));
|
||||
IsNull::No
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, Sqlite> for DbId {
|
||||
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
|
||||
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
|
||||
Ok(u128::from_be_bytes(bytes).into())
|
||||
}
|
||||
}
|
||||
|
||||
//-************************************************************************
|
||||
// serde traits
|
||||
//-************************************************************************
|
||||
impl Serialize for DbId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.bytes())
|
||||
}
|
||||
}
|
||||
|
||||
struct DbIdVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for DbIdVisitor {
|
||||
type Value = DbId;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("16 bytes")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match std::convert::TryInto::<[u8; 16]>::try_into(v) {
|
||||
Ok(v) => Ok(u128::from_be_bytes(v).into()),
|
||||
Err(_) => Err(serde::de::Error::invalid_length(v.len(), &self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
DbId::from_string(&v)
|
||||
.map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Str(&v), &self))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
DbId::from_string(v)
|
||||
.map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self))
|
||||
}
|
||||
|
||||
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let len = v.len();
|
||||
match std::convert::TryInto::<[u8; 16]>::try_into(v) {
|
||||
Ok(v) => Ok(u128::from_be_bytes(v).into()),
|
||||
Err(_) => Err(serde::de::Error::invalid_length(len, &self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut raw_bytes_from_db = [0u8; 16];
|
||||
let size = seq.size_hint().unwrap_or(0);
|
||||
let mut count = 0;
|
||||
while let Some(val) = seq.next_element()? {
|
||||
if count > 15 {
|
||||
break;
|
||||
}
|
||||
raw_bytes_from_db[count] = val;
|
||||
count += 1;
|
||||
}
|
||||
if count != 16 || size > 16 {
|
||||
let sz = if count < 16 { count } else { size };
|
||||
Err(serde::de::Error::invalid_length(sz, &self))
|
||||
} else {
|
||||
Ok(u128::from_be_bytes(raw_bytes_from_db).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DbId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_bytes(DbIdVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
//-************************************************************************
|
||||
// serialization tests
|
||||
//-************************************************************************
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_test::{assert_tokens, Token};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ser_de() {
|
||||
let bytes: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
let id: DbId = u128::from_be_bytes(bytes).into();
|
||||
|
||||
assert_tokens(
|
||||
&id,
|
||||
&[Token::Bytes(&[
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
])],
|
||||
);
|
||||
DbId(Uuid::from_u128(value))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,12 +52,18 @@ impl From<&ImportMovieOmega> for Watch {
|
|||
// utility functions for building CLI tools, currently just for benchmarking
|
||||
//-************************************************************************
|
||||
pub async fn add_watch_quests(pool: &SqlitePool, quests: &[WatchQuest]) -> Result<(), ()> {
|
||||
let mut builder = sqlx::QueryBuilder::new("insert into watch_quests (user, watch) ");
|
||||
let mut builder =
|
||||
sqlx::QueryBuilder::new("insert into watch_quests (id, user, watch, public, watched) ");
|
||||
builder.push_values(quests, |mut b, quest| {
|
||||
let id = DbId::new();
|
||||
let user = quest.user;
|
||||
let watch = quest.watch;
|
||||
//eprintln!("{user}, {watch}");
|
||||
b.push_bind(user).push_bind(watch);
|
||||
b.push_bind(id)
|
||||
.push_bind(user)
|
||||
.push_bind(watch)
|
||||
.push_bind(true)
|
||||
.push_bind(false);
|
||||
});
|
||||
|
||||
let q = builder.build();
|
||||
|
|
Loading…
Reference in a new issue