Use derive macro to create TableRow/PartialTableRow

This commit is contained in:
CanadianBaconBoi 2026-02-18 10:06:51 +01:00
parent e27a0d33d7
commit 5fedceb383
17 changed files with 328 additions and 822 deletions

View File

@ -9,6 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-db/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/cove-db/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/bin-test/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/bin-test/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-db/cove-db-macros/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

14
Cargo.lock generated
View File

@ -191,11 +191,21 @@ name = "cove-db"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cove-db-macros",
"cove-net-common", "cove-net-common",
"serde_json", "serde_json",
"sqlx", "sqlx",
] ]
[[package]]
name = "cove-db-macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "cove-net-client" name = "cove-net-client"
version = "0.1.0" version = "0.1.0"
@ -1863,9 +1873,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.114" version = "2.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -3,8 +3,10 @@ resolver = "3"
members = [ members = [
"cove-net/common", "cove-net/common",
"cove-net/client", "cove-net/client",
"cove-net/server" "cove-net/server",
, "bin-test", "cove-db"] "bin-test",
"cove-db",
"cove-db/cove-db-macros"]
[workspace.dependencies] [workspace.dependencies]
@ -13,6 +15,7 @@ async-trait = "0.1.89"
anyhow = "1.0.101" anyhow = "1.0.101"
hyper = { version = "1", features = ["full"] } hyper = { version = "1", features = ["full"] }
http-body-util = { version = "0.1.3", features = ["full"] } http-body-util = { version = "0.1.3", features = ["full"] }
rand = "0.10.0"
cove-net-common = {path = "cove-net/common"} cove-net-common = {path = "cove-net/common"}
serde_json = "1.0.149" serde_json = "1.0.149"
@ -26,3 +29,8 @@ hyper-util = { version = "0.1", features = ["full"] }
cove-db = {path = "cove-db"} cove-db = {path = "cove-db"}
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]} sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]}
cove-db-macros = {path = "cove-db/cove-db-macros" }
syn = {version = "2.0.116", features = ["full"]}
quote = {version = "1.0.44"}
proc-macro2 = "1.0.106"

View File

@ -9,5 +9,5 @@ cove-net-common.workspace = true
tokio.workspace = true tokio.workspace = true
cove-db.workspace = true cove-db.workspace = true
sqlx.workspace = true sqlx.workspace = true
anyhow = "1.0.101" anyhow.workspace = true
scc = "3.5.6" scc.workspace = true

View File

@ -7,5 +7,6 @@ edition = "2024"
sqlx.workspace = true sqlx.workspace = true
anyhow.workspace = true anyhow.workspace = true
serde_json.workspace = true serde_json.workspace = true
cove-db-macros.workspace = true
cove-net-common.workspace = true cove-net-common.workspace = true

View File

@ -0,0 +1,12 @@
[package]
name = "cove-db-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
syn.workspace = true
quote.workspace = true
proc-macro2.workspace = true

View File

@ -0,0 +1,257 @@
use proc_macro::{TokenStream};
use quote::{format_ident, quote};
use syn::{parse_str, Data, DeriveInput, Fields};
#[proc_macro_derive(DeriveTableRow, attributes(table_name))]
pub fn derive_table_row(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_derive_table_row(ast)
}
fn impl_derive_table_row(ast: DeriveInput) -> TokenStream {
let table_name: String = {
if ast.attrs.len() != 1 {
return quote!{compile_error!("The DeriveTableRow macro expects exactly one (1) table_name attribute")}.into();
}
let attr = ast.attrs.first().unwrap();
if attr.path().is_ident("table_name") {
if let Ok(args) = attr.parse_args::<syn::LitStr>() {
args.value()
} else {
return quote!{compile_error!("The DeriveTableRow macro's attribute table_name should only consist of a string")}.into();
}
} else {
return quote!{compile_error!("Incorrect attribute name for table_name attribute")}.into();
}
};
let name = ast.ident;
let partial_name = format_ident!("Partial{}", name);
let data = ast.data;
let fields = match data {
Data::Struct(data) => {
match data.fields {
Fields::Named(named_fields) => {
named_fields
},
_ => return quote!{compile_error!("The DeriveTableRow macro can only apply to structs solely consisting of named fields")}.into()
}
},
_ => return quote!{compile_error!("The DeriveTableRow macro can only be applied to structs.")}.into()
};
let struct_partial_row = {
let struct_partial_row_body = {
let mut ret = quote!();
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
ret.extend(quote! {
pub #name: Option<#ty>,
});
}
ret
};
quote! {
#[derive(Default)]
pub struct #partial_name {
#struct_partial_row_body
}
}
};
let impl_table_row = quote! {
impl crate::rows::TableRow for #name {
type Error = anyhow::Error;
type PartialRow = #partial_name;
fn get_table_name() -> &'static str {
#table_name
}
}
};
let impl_partial_table_row = quote! {
impl crate::rows::PartialTableRow for #partial_name {
type Error = anyhow::Error;
type FullTableRow = #name;
}
};
let impl_insertable_row = {
let insert_body = {
let mut ret = quote!(
crate::part::insert::SqlInsert::with_table(<Self as crate::rows::TableRow>::get_table_name())
);
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
let quoted_name = format!("{}", name);
let ty = &field.ty;
ret.extend(
quote! {
.col::<#ty>(#quoted_name)
}
)
}
ret
};
let bind_body = {
let mut ret = quote! {
query
};
let mut idx = 0;
let max_idx = fields.named.len();
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
ret.extend(quote! {
.bind(&self.#name)
});
if idx < max_idx-1 {
ret.extend(parse_str::<proc_macro2::TokenStream>("?"));
}
idx += 1;
}
ret
};
quote! {
impl crate::rows::InsertableRow for #name {
fn insert(&'_ self) -> crate::part::insert::SqlInsert {
#insert_body
}
fn bind<'a>(&'a self, query: crate::part::BindQuery<'a>) -> Result<crate::part::BindQuery<'a>, Self::Error> {
#bind_body
}
}
}
};
let impl_selectable_row = quote! {
impl crate::rows::SelectableRow for #partial_name {}
};
let impl_where_row = {
let match_body = {
let mut ret = quote!();
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
let quoted_name = format!("{}", name);
ret.extend(
quote! {
#quoted_name => query.bind(self.#name.as_ref().unwrap())?,
}
)
}
ret
};
quote! {
impl crate::rows::WhereRow for #partial_name {
fn bind<'a>(&'a self, wheres: crate::part::sql_where::SqlWhere, query: crate::part::BindQuery<'a>) -> Result<crate::part::BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
#match_body
_ => return Err(anyhow::anyhow!("No column {col} exists for table users"))
}
}
Ok(query)
}
}
}
};
let impl_try_from_pgrow_for_full = {
let try_from_body = {
let mut ret = quote!();
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
let quoted_name = format!("{}", name);
ret.extend(
quote! {
#name: row.try_get(#quoted_name)?,
}
)
}
ret
};
quote! {
impl TryFrom<sqlx::postgres::PgRow> for #name {
type Error = anyhow::Error;
fn try_from(row: sqlx::postgres::PgRow) -> Result<Self, anyhow::Error> {
use sqlx::Row;
Ok(Self {
#try_from_body
})
}
}
}
};
let impl_from_pgrow_for_partial = {
let from_body = {
let mut ret = quote!();
for field in fields.named.iter() {
let name = field.ident.as_ref().unwrap();
let quoted_name = format!("{}", name);
ret.extend(
quote! {
#name: row.try_get(#quoted_name).ok(),
}
)
}
ret
};
quote! {
impl From<sqlx::postgres::PgRow> for #partial_name {
fn from(row: sqlx::postgres::PgRow) -> Self {
use sqlx::Row;
Self {
#from_body
}
}
}
}
};
let mut ret = quote!();
ret.extend(struct_partial_row);
ret.extend(impl_table_row);
ret.extend(impl_partial_table_row);
ret.extend(impl_insertable_row);
ret.extend(impl_selectable_row);
ret.extend(impl_where_row);
ret.extend(impl_try_from_pgrow_for_full);
ret.extend(impl_from_pgrow_for_partial);
ret.into()
}

View File

@ -1,5 +1,5 @@
use anyhow::{Error}; use anyhow::{Error};
use sqlx::{Execute, Executor}; use sqlx::{Execute};
use sqlx::postgres::{ PgQueryResult}; use sqlx::postgres::{ PgQueryResult};
use cove_net_common::id::message_type::MessageType; use cove_net_common::id::message_type::MessageType;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;

View File

@ -1,13 +1,8 @@
use anyhow::anyhow; use cove_db_macros::DeriveTableRow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::select::SqlSelect;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
#[derive(DeriveTableRow)]
#[table_name("attachments")]
pub struct AttachmentRow { pub struct AttachmentRow {
pub id: SnowflakeID, pub id: SnowflakeID,
pub uploader_id: SnowflakeID, pub uploader_id: SnowflakeID,
@ -15,63 +10,3 @@ pub struct AttachmentRow {
pub file_size: i32, pub file_size: i32,
pub cdn_url: String pub cdn_url: String
} }
impl TableRow for AttachmentRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"attachments"
}
}
impl InsertableRow for AttachmentRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<SnowflakeID>("uploader_id")
.opt_col::<SnowflakeID>("channel_id")
.col::<i32>("file_size")
.col::<String>("cdn_url")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.uploader_id)?
.bind(&self.channel_id)?
.bind(&self.file_size)?
.bind(&self.cdn_url)
}
}
impl SelectableRow for AttachmentRow {}
impl WhereRow for AttachmentRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"uploader_id" => query.bind(&self.uploader_id)?,
"channel_id" => query.bind(&self.channel_id)?,
"file_size" => query.bind(&self.file_size)?,
"cdn_url" => query.bind(&self.cdn_url)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for AttachmentRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
uploader_id: value.try_get("uploader_id")?,
channel_id: value.try_get("channel_id")?,
file_size: value.try_get("file_size")?,
cdn_url: value.try_get("cdn_url")?,
})
}
}

View File

@ -1,15 +1,11 @@
use anyhow::anyhow;
use serde_json::Value; use serde_json::Value;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use cove_net_common::id::types::channel::ChannelMessageType; use cove_net_common::id::types::channel::ChannelMessageType;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
#[derive(DeriveTableRow)]
#[table_name("channels")]
pub struct ChannelRow { pub struct ChannelRow {
pub id: SnowflakeID, pub id: SnowflakeID,
pub guild_id: Option<SnowflakeID>, pub guild_id: Option<SnowflakeID>,
@ -38,119 +34,3 @@ pub struct ChannelRow {
pub user_limit: i32, pub user_limit: i32,
pub region: Option<String> pub region: Option<String>
} }
impl TableRow for ChannelRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"channels"
}
}
impl InsertableRow for ChannelRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.opt_col::<SnowflakeID>("guild_id")
.opt_col::<SnowflakeID>("parent_id")
.col::<String>("name")
.col::<ChannelMessageType>("channel_type")
.col::<i32>("position")
.col::<Value>("permission_overwrites")
.col::<i32>("rate_limit_per_user")
.col::<bool>("nsfw")
.col::<bool>("loud")
.opt_col::<Value>("thread_metadata")
.col::<i32>("member_count")
.col::<i32>("message_count")
.opt_col::<SnowflakeID>("thread_owner_id")
.col::<OffsetDateTime>("created_at")
.col::<SnowflakeID>("updated_at")
.opt_col::<String>("topic")
.col::<i32>("user_limit")
.opt_col::<String>("region")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.guild_id)?
.bind(&self.parent_id)?
.bind(&self.name)?
.bind(&self.channel_type)?
.bind(&self.position)?
.bind(&self.permission_overwrites)?
.bind(&self.rate_limit_per_user)?
.bind(&self.nsfw)?
.bind(&self.loud)?
.bind(&self.thread_metadata)?
.bind(&self.member_count)?
.bind(&self.message_count)?
.bind(&self.thread_owner_id)?
.bind(&self.created_at)?
.bind(&self.updated_at)?
.bind(&self.topic)?
.bind(&self.user_limit)?
.bind(&self.region)
}
}
impl SelectableRow for ChannelRow {}
impl WhereRow for ChannelRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"guild_id" => query.bind(&self.guild_id)?,
"parent_id" => query.bind(&self.parent_id)?,
"name" => query.bind(&self.name)?,
"channel_type" => query.bind(&self.channel_type)?,
"position" => query.bind(&self.position)?,
"permission_overwrites" => query.bind(&self.permission_overwrites)?,
"rate_limit_per_user" => query.bind(&self.rate_limit_per_user)?,
"nsfw" => query.bind(&self.nsfw)?,
"loud" => query.bind(&self.loud)?,
"thread_metadata" => query.bind(&self.thread_metadata)?,
"member_count" => query.bind(&self.member_count)?,
"message_count" => query.bind(&self.message_count)?,
"thread_owner_id" => query.bind(&self.thread_owner_id)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
"topic" => query.bind(&self.topic)?,
"user_limit" => query.bind(&self.user_limit)?,
"region" => query.bind(&self.region)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for ChannelRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self{
id: value.try_get("id")?,
guild_id: value.try_get("guild_id")?,
parent_id: value.try_get("parent_id")?,
name: value.try_get("name")?,
channel_type: value.try_get("channel_type")?,
position: value.try_get("position")?,
permission_overwrites: value.try_get("permission_overwrites")?,
rate_limit_per_user: value.try_get("rate_limit_per_user")?,
nsfw: value.try_get("nsfw")?,
loud: value.try_get("loud")?,
thread_metadata: value.try_get("thread_metadata")?,
member_count: value.try_get("member_count")?,
message_count: value.try_get("message_count")?,
thread_owner_id: value.try_get("thread_owner_id")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
topic: value.try_get("topic")?,
user_limit: value.try_get("user_limit")?,
region: value.try_get("region")?,
})
}
}

View File

@ -1,17 +1,13 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::guild::component::default_notification::DefaultMessageNotificationSetting; use cove_net_common::guild::component::default_notification::DefaultMessageNotificationSetting;
use cove_net_common::guild::component::explicit_filter::ExplicitContentFilterSetting; use cove_net_common::guild::component::explicit_filter::ExplicitContentFilterSetting;
use cove_net_common::guild::component::verification_level::VerificationLevelSetting; use cove_net_common::guild::component::verification_level::VerificationLevelSetting;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct GuildRow { #[derive(DeriveTableRow)]
#[table_name("guilds")]
pub struct GuildRow {
pub id: SnowflakeID, pub id: SnowflakeID,
pub name: String, pub name: String,
@ -50,151 +46,3 @@ struct GuildRow {
pub large: bool, pub large: bool,
pub member_count: i32 pub member_count: i32
} }
impl TableRow for GuildRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"guilds"
}
}
impl InsertableRow for GuildRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<String>("name")
.opt_col::<String>("description")
.opt_col::<SnowflakeID>("icon")
.opt_col::<SnowflakeID>("banner")
.opt_col::<SnowflakeID>("splash")
.col::<SnowflakeID>("owner_id")
.opt_col::<[u8; 8]>("owner_permissions")
.col::<String>("region")
.col::<i32>("features")
.opt_col::<SnowflakeID>("afk_channel_id")
.col::<i32>("afk_timeout")
.col::<VerificationLevelSetting>("verification_level")
.col::<DefaultMessageNotificationSetting>("default_message_notifications")
.col::<ExplicitContentFilterSetting>("explicit_content_filter")
.opt_col::<SnowflakeID>("system_channel_id")
.opt_col::<i32>("system_channel_flags")
.arr_col::<SnowflakeID>("premium_boosters")
.col::<i32>("premium_tier")
.col::<i32>("premium_booster_count")
.col::<bool>("widget_enabled")
.opt_col::<SnowflakeID>("widget_channel_id")
.col::<String>("preferred_locale")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
.col::<bool>("large")
.col::<i32>("member_count")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.name)?
.bind(&self.description)?
.bind(&self.icon)?
.bind(&self.banner)?
.bind(&self.splash)?
.bind(&self.owner_id)?
.bind(&self.owner_permissions)?
.bind(&self.region)?
.bind(&self.features)?
.bind(&self.afk_channel_id)?
.bind(&self.afk_timeout)?
.bind(&self.verification_level)?
.bind(&self.default_message_notifications)?
.bind(&self.explicit_content_filter)?
.bind(&self.system_channel_id)?
.bind(&self.system_channel_flags)?
.bind(&self.premium_boosters)?
.bind(&self.premium_tier)?
.bind(&self.premium_booster_count)?
.bind(&self.widget_enabled)?
.bind(&self.widget_channel_id)?
.bind(&self.preferred_locale)?
.bind(&self.created_at)?
.bind(&self.updated_at)?
.bind(&self.large)?
.bind(&self.member_count)
}
}
impl SelectableRow for GuildRow {}
impl WhereRow for GuildRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"name" => query.bind(&self.name)?,
"description" => query.bind(&self.description)?,
"icon" => query.bind(&self.icon)?,
"banner" => query.bind(&self.banner)?,
"splash" => query.bind(&self.splash)?,
"owner_id" => query.bind(&self.owner_id)?,
"owner_permissions" => query.bind(&self.owner_permissions)?,
"region" => query.bind(&self.region)?,
"features" => query.bind(&self.features)?,
"afk_channel_id" => query.bind(&self.afk_channel_id)?,
"afk_timeout" => query.bind(&self.afk_timeout)?,
"verification_level" => query.bind(&self.verification_level)?,
"default_message_notifications" => query.bind(&self.default_message_notifications)?,
"explicit_content_filter" => query.bind(&self.explicit_content_filter)?,
"system_channel_id" => query.bind(&self.system_channel_id)?,
"system_channel_flags" => query.bind(&self.system_channel_flags)?,
"premium_boosters" => query.bind(&self.premium_boosters)?,
"premium_tier" => query.bind(&self.premium_tier)?,
"premium_booster_count" => query.bind(&self.premium_booster_count)?,
"widget_enabled" => query.bind(&self.widget_enabled)?,
"widget_channel_id" => query.bind(&self.widget_channel_id)?,
"preferred_locale" => query.bind(&self.preferred_locale)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
"large" => query.bind(&self.large)?,
"member_count" => query.bind(&self.member_count)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for GuildRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
name: value.try_get("name")?,
description: value.try_get("description")?,
icon: value.try_get("icon")?,
banner: value.try_get("banner")?,
splash: value.try_get("splash")?,
owner_id: value.try_get("owner_id")?,
owner_permissions: value.try_get("owner_permissions")?,
region: value.try_get("region")?,
features: value.try_get("features")?,
afk_channel_id: value.try_get("afk_channel_id")?,
afk_timeout: value.try_get("afk_timeout")?,
verification_level: value.try_get("verification_level")?,
default_message_notifications: value.try_get("default_message_notifications")?,
explicit_content_filter: value.try_get("explicit_content_filter")?,
system_channel_id: value.try_get("system_channel_id")?,
system_channel_flags: value.try_get("system_channel_flags")?,
premium_boosters: value.try_get("premium_boosters")?,
premium_tier: value.try_get("premium_tier")?,
premium_booster_count: value.try_get("premium_booster_count")?,
widget_enabled: value.try_get("widget_enabled")?,
widget_channel_id: value.try_get("widget_channel_id")?,
preferred_locale: value.try_get("preferred_locale")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
large: value.try_get("large")?,
member_count: value.try_get("member_count")?,
})
}
}

View File

@ -1,14 +1,10 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct GuildMemberRow { #[derive(DeriveTableRow)]
#[table_name("guild_members")]
pub struct GuildMemberRow {
pub guild_id: SnowflakeID, pub guild_id: SnowflakeID,
pub user_id: SnowflakeID, pub user_id: SnowflakeID,
@ -27,91 +23,3 @@ struct GuildMemberRow {
pub updated_at: OffsetDateTime, pub updated_at: OffsetDateTime,
} }
impl TableRow for GuildMemberRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"guild_members"
}
}
impl InsertableRow for GuildMemberRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("guild_id")
.col::<SnowflakeID>("user_id")
.opt_col::<String>("nick")
.col::<OffsetDateTime>("joined_at")
.arr_col::<SnowflakeID>("roles")
.opt_col::<OffsetDateTime>("boosting_since")
.opt_col::<SnowflakeID>("voice_channel_id")
.col::<bool>("deafened")
.col::<bool>("muted")
.col::<bool>("pending")
.opt_col::<OffsetDateTime>("timed_out_until")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.guild_id)?
.bind(&self.user_id)?
.bind(&self.nick)?
.bind(&self.joined_at)?
.bind(&self.roles)?
.bind(&self.boosting_since)?
.bind(&self.voice_channel_id)?
.bind(&self.deafened)?
.bind(&self.muted)?
.bind(&self.pending)?
.bind(&self.timed_out_until)?
.bind(&self.updated_at)
}
}
impl SelectableRow for GuildMemberRow {}
impl WhereRow for GuildMemberRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"guild_id" => query.bind(&self.guild_id)?,
"user_id" => query.bind(&self.user_id)?,
"nick" => query.bind(&self.nick)?,
"joined_at" => query.bind(&self.joined_at)?,
"roles" => query.bind(&self.roles)?,
"boosting_since" => query.bind(&self.boosting_since)?,
"voice_channel_id" => query.bind(&self.voice_channel_id)?,
"deafened" => query.bind(&self.deafened)?,
"muted" => query.bind(&self.muted)?,
"pending" => query.bind(&self.pending)?,
"timed_out_until" => query.bind(&self.timed_out_until)?,
"updated_at" => query.bind(&self.updated_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for GuildMemberRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
guild_id: value.try_get("guild_id")?,
user_id: value.try_get("user_id")?,
nick: value.try_get("nick")?,
joined_at: value.try_get("joined_at")?,
roles: value.try_get("roles")?,
boosting_since: value.try_get("boosting_since")?,
voice_channel_id: value.try_get("voice_channel_id")?,
deafened: value.try_get("deafened")?,
muted: value.try_get("muted")?,
pending: value.try_get("pending")?,
timed_out_until: value.try_get("timed_out_until")?,
updated_at: value.try_get("updated_at")?,
})
}
}

View File

@ -1,15 +1,11 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use cove_net_common::id::types::text::TextMessageType; use cove_net_common::id::types::text::TextMessageType;
use cove_net_common::message::c2s::text::text::TextEmbed; use cove_net_common::message::c2s::text::text::TextEmbed;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
#[derive(DeriveTableRow)]
#[table_name("messages")]
pub struct MessageRow { pub struct MessageRow {
pub id: SnowflakeID, pub id: SnowflakeID,
pub channel_id: SnowflakeID, pub channel_id: SnowflakeID,
@ -37,119 +33,3 @@ pub struct MessageRow {
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime, pub updated_at: OffsetDateTime,
} }
impl TableRow for MessageRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"messages"
}
}
impl InsertableRow for MessageRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<SnowflakeID>("channel_id")
.opt_col::<SnowflakeID>("guild_id")
.col::<SnowflakeID>("author_id")
.col::<String>("content")
.col::<OffsetDateTime>("timestamp")
.opt_col::<OffsetDateTime>("edited_timestamp")
.col::<bool>("tts")
.opt_arr_col::<SnowflakeID>("mentions")
.col::<bool>("mention_everyone")
.opt_arr_col::<TextEmbed>("embeds")
.opt_arr_col::<SnowflakeID>("attachments")
.opt_col::<SnowflakeID>("reply_message_id")
.opt_col::<SnowflakeID>("application_id")
.col::<TextMessageType>("message_type")
.opt_col::<String>("thread_name")
.opt_col::<i32>("auto_archive_duration")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.channel_id)?
.bind(&self.guild_id)?
.bind(&self.author_id)?
.bind(&self.content)?
.bind(&self.timestamp)?
.bind(&self.edited_timestamp)?
.bind(&self.tts)?
.bind(&self.mentions)?
.bind(&self.mention_everyone)?
.bind(&self.embeds)?
.bind(&self.attachments)?
.bind(&self.reply_message_id)?
.bind(&self.application_id)?
.bind(&self.message_type)?
.bind(&self.thread_name)?
.bind(&self.auto_archive_duration)?
.bind(&self.created_at)?
.bind(&self.updated_at)
}
}
impl SelectableRow for MessageRow {}
impl WhereRow for MessageRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"channel_id" => query.bind(&self.channel_id)?,
"guild_id" => query.bind(&self.guild_id)?,
"author_id" => query.bind(&self.author_id)?,
"content" => query.bind(&self.content)?,
"timestamp" => query.bind(&self.timestamp)?,
"edited_timestamp" => query.bind(&self.edited_timestamp)?,
"tts" => query.bind(&self.tts)?,
"mentions" => query.bind(&self.mentions)?,
"mention_everyone" => query.bind(&self.mention_everyone)?,
"embeds" => query.bind(&self.embeds)?,
"attachments" => query.bind(&self.attachments)?,
"reply_message_id" => query.bind(&self.reply_message_id)?,
"application_id" => query.bind(&self.application_id)?,
"message_type" => query.bind(&self.message_type)?,
"thread_name" => query.bind(&self.thread_name)?,
"auto_archive_duration" => query.bind(&self.auto_archive_duration)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for MessageRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
channel_id: value.try_get("channel_id")?,
guild_id: value.try_get("guild_id")?,
author_id: value.try_get("author_id")?,
content: value.try_get("content")?,
timestamp: value.try_get("timestamp")?,
edited_timestamp: value.try_get("edited_timestamp")?,
tts: value.try_get("tts")?,
mentions: value.try_get("mentions")?,
mention_everyone: value.try_get("mention_everyone")?,
embeds: value.try_get("embeds")?,
attachments: value.try_get("attachments")?,
reply_message_id: value.try_get("reply_message_id")?,
application_id: value.try_get("application_id")?,
message_type: value.try_get("message_type")?,
thread_name: value.try_get("thread_name")?,
auto_archive_duration: value.try_get("auto_archive_duration")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
})
}
}

View File

@ -3,6 +3,12 @@ use crate::part::insert::SqlInsert;
use crate::part::select::SqlSelect; use crate::part::select::SqlSelect;
use crate::part::sql_where::SqlWhere; use crate::part::sql_where::SqlWhere;
pub mod attachment;
pub mod channel;
pub mod guild;
pub mod guild_member;
pub mod message;
pub mod nonce;
pub mod user; pub mod user;
pub trait TableRow { pub trait TableRow {

View File

@ -1,77 +1,13 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct NonceRow { #[derive(DeriveTableRow)]
#[table_name("pending_nonces")]
pub struct NonceRow {
pub nonce: String, pub nonce: String,
pub channel_id: SnowflakeID, pub channel_id: SnowflakeID,
pub author_id: SnowflakeID, pub author_id: SnowflakeID,
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
pub expires_at: OffsetDateTime, pub expires_at: OffsetDateTime,
} }
impl TableRow for NonceRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"pending_nonces"
}
}
impl InsertableRow for NonceRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<String>("nonce")
.col::<SnowflakeID>("channel_id")
.col::<SnowflakeID>("author_id")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("expires_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.nonce)?
.bind(&self.channel_id)?
.bind(&self.author_id)?
.bind(&self.created_at)?
.bind(&self.expires_at)
}
}
impl SelectableRow for NonceRow {}
impl WhereRow for NonceRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"nonce" => query.bind(&self.nonce)?,
"channel_id" => query.bind(&self.channel_id)?,
"author_id" => query.bind(&self.author_id)?,
"created_at" => query.bind(&self.created_at)?,
"expires_at" => query.bind(&self.expires_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for NonceRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
nonce: value.try_get("nonce")?,
channel_id: value.try_get("channel_id")?,
author_id: value.try_get("author_id")?,
created_at: value.try_get("created_at")?,
expires_at: value.try_get("expires_at")?,
})
}
}

View File

@ -1,15 +1,11 @@
use anyhow::anyhow;
use serde_json::Value; use serde_json::Value;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use cove_db_macros::DeriveTableRow;
use cove_net_common::id::SnowflakeID; use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, PartialTableRow, SelectableRow, TableRow, WhereRow};
use crate::types::user_status::UserStatus; use crate::types::user_status::UserStatus;
#[derive(DeriveTableRow)]
#[table_name("users")]
pub struct UserRow { pub struct UserRow {
pub id: SnowflakeID, pub id: SnowflakeID,
pub username: String, pub username: String,
@ -31,175 +27,3 @@ pub struct UserRow {
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime, pub updated_at: OffsetDateTime,
} }
#[derive(Default)]
pub struct PartialUserRow {
pub id: Option<SnowflakeID>,
pub username: Option<String>,
pub discriminator: Option<String>,
pub avatar_hash: Option<Option<String>>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub password_hash: Option<String>,
pub mfa_enabled: Option<bool>,
pub mfa_secret: Option<Option<String>>,
pub status: Option<UserStatus>,
pub public_flags: Option<i64>,
pub locale: Option<String>,
pub premium_since: Option<Option<OffsetDateTime>>,
pub premium_end: Option<Option<OffsetDateTime>>,
pub bot: Option<bool>,
pub bot_oauth_scopes: Option<Value>,
pub preferences: Option<Value>,
pub created_at: Option<OffsetDateTime>,
pub updated_at: Option<OffsetDateTime>,
}
impl TableRow for UserRow {
type Error = anyhow::Error;
type PartialRow = PartialUserRow;
fn get_table_name() -> &'static str {
"users"
}
}
impl PartialTableRow for PartialUserRow {
type Error = anyhow::Error;
type FullTableRow = UserRow;
}
impl InsertableRow for UserRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<String>("username")
.col::<String>("discriminator")
.opt_col::<String>("avatar_hash")
.col::<String>("email")
.col::<bool>("email_verified")
.col::<String>("password_hash")
.col::<bool>("mfa_enabled")
.opt_col::<String>("mfa_secret")
.col::<UserStatus>("status")
.col::<i64>("public_flags")
.col::<String>("locale")
.opt_col::<OffsetDateTime>("premium_since")
.opt_col::<OffsetDateTime>("premium_end")
.col::<bool>("bot")
.col::<Value>("bot_oauth_scopes")
.col::<Value>("preferences")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.username)?
.bind(&self.discriminator)?
.bind(&self.avatar_hash)?
.bind(&self.email)?
.bind(&self.email_verified)?
.bind(&self.password_hash)?
.bind(&self.mfa_enabled)?
.bind(&self.mfa_secret)?
.bind(&self.status)?
.bind(&self.public_flags)?
.bind(&self.locale)?
.bind(&self.premium_since)?
.bind(&self.premium_end)?
.bind(&self.bot)?
.bind(&self.bot_oauth_scopes)?
.bind(&self.preferences)?
.bind(&self.created_at)?
.bind(&self.updated_at)
}
}
impl SelectableRow for PartialUserRow {}
impl WhereRow for PartialUserRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(self.id.as_ref().unwrap())?,
"username" => query.bind(self.username.as_ref().unwrap())?,
"discriminator" => query.bind(self.discriminator.as_ref().unwrap())?,
"avatar_hash" => query.bind(self.avatar_hash.as_ref().unwrap())?,
"email" => query.bind(self.email.as_ref().unwrap())?,
"email_verified" => query.bind(self.email_verified.as_ref().unwrap())?,
"password_hash" => query.bind(self.password_hash.as_ref().unwrap())?,
"mfa_enabled" => query.bind(self.mfa_enabled.as_ref().unwrap())?,
"mfa_secret" => query.bind(self.mfa_secret.as_ref().unwrap())?,
"status" => query.bind(self.status.as_ref().unwrap())?,
"public_flags" => query.bind(self.public_flags.as_ref().unwrap())?,
"locale" => query.bind(self.locale.as_ref().unwrap())?,
"premium_since" => query.bind(self.premium_since.as_ref().unwrap())?,
"premium_end" => query.bind(self.premium_end.as_ref().unwrap())?,
"bot" => query.bind(self.bot.as_ref().unwrap())?,
"bot_oauth_scopes" => query.bind(self.bot_oauth_scopes.as_ref().unwrap())?,
"preferences" => query.bind(self.preferences.as_ref().unwrap())?,
"created_at" => query.bind(self.created_at.as_ref().unwrap())?,
"updated_at" => query.bind(self.updated_at.as_ref().unwrap())?,
_ => return Err(anyhow!("No column {col} exists for table users"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for UserRow {
type Error = anyhow::Error;
fn try_from(row: PgRow) -> Result<Self, anyhow::Error> {
Ok(Self {
id: row.try_get("id")?,
username: row.try_get("username")?,
discriminator: row.try_get("discriminator")?,
avatar_hash: row.try_get("avatar_hash")?,
email: row.try_get("email")?,
email_verified: row.try_get("email_verified")?,
password_hash: row.try_get("password_hash")?,
mfa_enabled: row.try_get("mfa_enabled")?,
mfa_secret: row.try_get("mfa_secret")?,
status: row.try_get("status")?,
public_flags: row.try_get("public_flags")?,
locale: row.try_get("locale")?,
premium_since: row.try_get("premium_since")?,
premium_end: row.try_get("premium_end")?,
bot: row.try_get("bot")?,
bot_oauth_scopes: row.try_get("bot_oauth_scopes")?,
preferences: row.try_get("preferences")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
})
}
}
impl From<PgRow> for PartialUserRow {
fn from(row: PgRow) -> Self {
Self {
id: row.try_get("id").ok(),
username: row.try_get("username").ok(),
discriminator: row.try_get("discriminator").ok(),
avatar_hash: row.try_get("avatar_hash").ok(),
email: row.try_get("email").ok(),
email_verified: row.try_get("email_verified").ok(),
password_hash: row.try_get("password_hash").ok(),
mfa_enabled: row.try_get("mfa_enabled").ok(),
mfa_secret: row.try_get("mfa_secret").ok(),
status: row.try_get("status").ok(),
public_flags: row.try_get("public_flags").ok(),
locale: row.try_get("locale").ok(),
premium_since: row.try_get("premium_since").ok(),
premium_end: row.try_get("premium_end").ok(),
bot: row.try_get("bot").ok(),
bot_oauth_scopes: row.try_get("bot_oauth_scopes").ok(),
preferences: row.try_get("preferences").ok(),
created_at: row.try_get("created_at").ok(),
updated_at: row.try_get("updated_at").ok(),
}
}
}

View File

@ -6,7 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
hex.workspace = true hex.workspace = true
rand = "0.10.0" rand.workspace = true
serde.workspace = true serde.workspace = true
serde_with.workspace = true serde_with.workspace = true
serde_json.workspace = true serde_json.workspace = true