Fixed query building and dice track

This commit is contained in:
2024-12-22 16:59:35 -05:00
parent bd132d0c6b
commit b218cd50f2
11 changed files with 304 additions and 160 deletions

View File

@@ -11,5 +11,5 @@ post {
}
auth:bearer {
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjI1MDg0MjI2MTIyMTI3NzY5NywibmFtZSI6ImJzaGVycmlmZiIsImlhdCI6MTczNDg0MzA0NywiZXhwIjoxNzM0OTI5NDQ3LCJqdGkiOiIycEowbmN5YmF1TVo4TG1aQ0VwU1B2OWgzMXFzU1FwaCJ9.gYf6oAm2POBXOHUnG4dTy5maKxTjUk8WxawOrIafjEE
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjI1MDg0MjI2MTIyMTI3NzY5NywibmFtZSI6ImJzaGVycmlmZiIsImlhdCI6MTczNDkwMjgzOSwiZXhwIjoxNzM0OTg5MjM5LCJqdGkiOiJWTlFjeXpBN25sZEt1SWtzcDFzc1pRNHNacUZ2dWZPZCJ9.JnO-Rklv9YZKWjRvehR4-tfP1dlO5vIEWpSh_W4xZWY
}

View File

@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::api::auth::{csprng, AuthCredential};
use crate::api::auth::AuthorizationMiddleware;
use crate::AppState;
use crate::data::ExecutableQuery;
use crate::data::condition::Condition;
use crate::data::insert::InsertBuilder;
use crate::data::query::QueryBuilder;
@@ -52,7 +53,8 @@ impl ApiKey {
.column("access_mask", Value::Int(self.access_mask))
.column("created_at", Value::DateTime(self.created_at))
.column("last_used_at", Value::OptionalDateTime(self.last_used_at))
.execute().await?;
.execute()
.await?;
Ok(())
}
@@ -64,7 +66,9 @@ impl ApiKey {
.column("created_at", Value::DateTime(self.created_at))
.column("last_used_at", Value::OptionalDateTime(self.last_used_at))
.where_condition(Condition::is_equal("key", Value::Text(self.key.clone())))
.execute().await {
.execute()
.await
{
Ok(_) => Ok(()),
Err(err) => {
log::error!("error: {}", err);
@@ -76,7 +80,8 @@ impl ApiKey {
pub async fn find_by_key(key: &str) -> Option<Self> {
QueryBuilder::new(TABLE_NAME)
.where_condition(Condition::is_equal("key", Value::Text(key.to_string())))
.fetch_optional().await
.fetch_optional()
.await
}
pub async fn delete_by_id(key: &str) -> SirenResult<()> {

View File

@@ -10,6 +10,8 @@ use uuid::Uuid;
use crate::api::auth::{AuthCredential, AuthorizationMiddleware};
use crate::AppState;
use crate::bot::commands::fun::roll::{format_roll, parse_dice};
use crate::data::condition::Condition;
use crate::data::{ExecutableQuery, Value};
use crate::data::query::QueryBuilder;
use crate::error::{Error, SirenResult};
@@ -95,22 +97,26 @@ struct QueryDiceTrack {
}
impl QueryDiceTrack {
pub async fn find() -> SirenResult<Vec<Self>> {
let pool = crate::data::pool();
let query = QueryBuilder::new(TABLE_NAME)
// .where_condition(
// Condition::and(
// Condition::is_equal("guild_id", "$1"),
// Condition::and(
// Condition::is_equal("owner_id", "$2"),
//
// )
// )
// )
.build();
let items: Vec<QueryDiceTrack> = sqlx::query_as(&query.0).fetch_all(pool).await?;
Ok(items)
pub async fn find(dice: &InsertDiceTrack) -> Option<Self> {
QueryBuilder::new(TABLE_NAME)
.where_condition(Condition::and(
Condition::is_equal("guild_id", Value::BigInt(dice.guild_id)),
Condition::and(
Condition::is_equal("owner_id", Value::BigInt(dice.owner_id)),
Condition::and(
Condition::is_equal("dice", Value::Text(dice.dice.clone())),
Condition::and(
Condition::is_equal("user_id", Value::OptionalBigInt(dice.user_id)),
Condition::and(
Condition::is_equal("value", Value::OptionalInt(dice.value)),
Condition::is_equal("operator", Value::OptionalText(dice.operator.clone())),
),
),
),
),
))
.fetch_optional()
.await
}
}
@@ -168,7 +174,7 @@ pub async fn add_track_dice(
let dice = parse_dice(&payload.dice)?;
let dice = InsertDiceTrack {
let insert_dice = InsertDiceTrack {
guild_id: guild_id.get() as i64,
owner_id: owner_id.get() as i64,
dice: format_roll(dice.0, dice.1, dice.2),
@@ -180,6 +186,14 @@ pub async fn add_track_dice(
},
};
let dice_track = dice.insert().await?;
// Check for existing dice tracks
let results = QueryDiceTrack::find(&insert_dice).await;
match results {
Some(dice_track) => Ok(Json(dice_track)),
None => {
let dice_track = insert_dice.insert().await?;
Ok(Json(dice_track))
}
}
}

View File

@@ -2,7 +2,6 @@ pub use app::App;
use std::sync::Arc;
use axum::Router;
use serde::Deserialize;
use crate::AppState;
mod app;

View File

@@ -24,12 +24,20 @@ impl Condition {
Condition::Group(Box::new(self))
}
pub fn is_equal(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} = ?", left), vec![right.into()])
pub fn is_equal(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} = ?", left), vec![right.into()]),
}
}
pub fn not_equal(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} != ?", left), vec![right.into()])
pub fn not_equal(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} != ?", left), vec![right.into()]),
}
}
pub fn is_null(value: &str) -> Self {
@@ -41,6 +49,11 @@ impl Condition {
}
pub fn is_in(left: &str, right: Vec<Value>) -> Self {
// Use helper function to handle special cases
if let Some(condition) = Self::handle_empty_or_all_none(left, &right, true) {
return condition;
}
let right_list = right
.iter()
.map(|v| "'?'".to_string())
@@ -50,6 +63,11 @@ impl Condition {
}
pub fn not_in(left: &str, right: Vec<Value>) -> Self {
// Use helper function to handle special cases
if let Some(condition) = Self::handle_empty_or_all_none(left, &right, true) {
return condition;
}
let right_list = right
.iter()
.map(|v| "'?'".to_string())
@@ -58,36 +76,122 @@ impl Condition {
Condition::Simple(format!("{} NOT IN ({})", left, right_list), right)
}
pub fn like(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} LIKE '?'", left), vec![right.into()])
pub fn like(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} LIKE '?'", left), vec![right.into()]),
}
}
pub fn not_like(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} NOT LIKE '?'", left), vec![right.into()])
pub fn not_like(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} NOT LIKE '?'", left), vec![right.into()]),
}
}
pub fn i_like(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} ILIKE '?'", left), vec![right.into()])
pub fn i_like(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} ILIKE '?'", left), vec![right.into()]),
}
}
pub fn not_i_like(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} NOT ILIKE '?'", left), vec![right.into()])
pub fn not_i_like(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} NOT ILIKE '?'", left), vec![right.into()]),
}
}
pub fn gt(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} > ?", left), vec![right.into()])
pub fn gt(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} > ?", left), vec![right.into()]),
}
}
pub fn gte(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} >= ?", left), vec![right.into()])
pub fn gte(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} >= ?", left), vec![right.into()]),
}
}
pub fn lt(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} < ?", left), vec![right.into()])
pub fn lt(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} < ?", left), vec![right.into()]),
}
}
pub fn lte(left: &str, right: impl Into<Value>) -> Self {
Condition::Simple(format!("{} <= ?", left), vec![right.into()])
pub fn lte(left: &str, right: impl Into<Value> + Clone) -> Self {
let value = right.clone().into();
match Self::from_optional_value(left, value) {
Some(condition) => condition,
None => Condition::Simple(format!("{} <= ?", left), vec![right.into()]),
}
}
// Private helper function to handle optional values
fn from_optional_value(left: &str, value: Value) -> Option<Self> {
match value {
Value::OptionalInt(None) => Some(Condition::is_null(left)),
Value::OptionalBigInt(None) => Some(Condition::is_null(left)),
Value::OptionalFloat(None) => Some(Condition::is_null(left)),
Value::OptionalDouble(None) => Some(Condition::is_null(left)),
Value::OptionalBool(None) => Some(Condition::is_null(left)),
Value::OptionalText(None) => Some(Condition::is_null(left)),
Value::OptionalDateTime(None) => Some(Condition::is_null(left)),
_ => None, // For non-optional or Some(value), let the primary method handle it
}
}
// Private helper to handle `empty` or `all-None` lists
fn handle_empty_or_all_none(left: &str, right: &[Value], negate: bool) -> Option<Self> {
if right.is_empty() {
// For an empty list, return an always-false condition
// NOT IN with empty list is always TRUE, but we're defaulting to SQL-SAFE result (FALSE)
return Some(Condition::Simple(
if negate {
"TRUE".to_string()
} else {
"FALSE".to_string()
},
vec![],
));
}
// Check if all elements in the `right` vector are `None` (Optional*)
if right.iter().all(|v| {
matches!(
v,
Value::OptionalInt(None)
| Value::OptionalBigInt(None)
| Value::OptionalFloat(None)
| Value::OptionalDouble(None)
| Value::OptionalBool(None)
| Value::OptionalText(None)
| Value::OptionalDateTime(None)
)
}) {
// If all values are None, handle as NULL or NOT NULL
return Some(if negate {
Condition::not_null(left)
} else {
Condition::is_null(left)
});
}
// Otherwise, this is not an empty or all-none case
None
}
pub fn to_sql(&self, mut counter: &mut usize) -> (String, Vec<Value>) {
@@ -96,12 +200,20 @@ impl Condition {
match self {
Condition::Simple(condition, values) => {
// Replace all instances of '?' with a numbered bind
let mut bind_index = *counter;
let numbered_condition = condition.replace("?", {
bind_index += 1;
&format!("${}", bind_index)
});
// Replace each instance of '?' with increasing numbered binds
let mut numbered_condition = String::new();
let mut chars = condition.chars().peekable();
while let Some(c) = chars.next() {
if c == '?' {
// Increment the counter and replace `?` with a numbered bind
*counter += 1;
numbered_condition.push_str(&format!("${}", *counter));
} else {
numbered_condition.push(c);
}
}
sql.push_str(&numbered_condition);
binds.extend(values.clone());
}

View File

@@ -0,0 +1,105 @@
use sqlx::{FromRow, Postgres};
use crate::data::Value;
pub trait ExecutableQuery {
fn build(&self) -> (String, Vec<Value>);
async fn execute(&self) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
// Build the SQL query and its values
let (query_string, values) = self.build();
// Start constructing the query
let mut query = sqlx::query(&query_string);
// Bind each value to its respective placeholder
for value in values {
match value {
Value::Int(n) => query = query.bind(n),
Value::OptionalInt(n) => query = query.bind(n),
Value::BigInt(n) => query = query.bind(n),
Value::OptionalBigInt(n) => query = query.bind(n),
Value::Float(n) => query = query.bind(n),
Value::OptionalFloat(n) => query = query.bind(n),
Value::Double(n) => query = query.bind(n),
Value::OptionalDouble(n) => query = query.bind(n),
Value::Bool(n) => query = query.bind(n),
Value::OptionalBool(n) => query = query.bind(n),
Value::Text(n) => query = query.bind(n),
Value::OptionalText(n) => query = query.bind(n),
Value::DateTime(n) => query = query.bind(n),
Value::OptionalDateTime(n) => query = query.bind(n),
}
}
let pool = crate::data::pool();
query.execute(pool).await
}
async fn fetch_optional<
T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>,
>(
&self,
) -> Option<T> {
let (query_string, values) = self.build();
let mut query_as = sqlx::query_as(&query_string);
for value in values {
match value {
Value::Int(n) => query_as = query_as.bind(n),
Value::OptionalInt(n) => query_as = query_as.bind(n),
Value::BigInt(n) => query_as = query_as.bind(n),
Value::OptionalBigInt(n) => query_as = query_as.bind(n),
Value::Float(n) => query_as = query_as.bind(n),
Value::OptionalFloat(n) => query_as = query_as.bind(n),
Value::Double(n) => query_as = query_as.bind(n),
Value::OptionalDouble(n) => query_as = query_as.bind(n),
Value::Bool(n) => query_as = query_as.bind(n),
Value::OptionalBool(n) => query_as = query_as.bind(n),
Value::Text(n) => query_as = query_as.bind(n),
Value::OptionalText(n) => query_as = query_as.bind(n),
Value::DateTime(n) => query_as = query_as.bind(n),
Value::OptionalDateTime(n) => query_as = query_as.bind(n),
}
}
let pool = crate::data::pool();
query_as.fetch_optional(pool).await.unwrap_or_else(|err| {
log::error!(
"Unable to fetch optional on query '{}': {}",
query_string,
err
);
None
})
}
async fn fetch_all<T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>>(
&self,
) -> Vec<T> {
let (query_string, values) = self.build();
let mut query_as = sqlx::query_as(&query_string);
for value in values {
match value {
Value::Int(n) => query_as = query_as.bind(n),
Value::OptionalInt(n) => query_as = query_as.bind(n),
Value::BigInt(n) => query_as = query_as.bind(n),
Value::OptionalBigInt(n) => query_as = query_as.bind(n),
Value::Float(n) => query_as = query_as.bind(n),
Value::OptionalFloat(n) => query_as = query_as.bind(n),
Value::Double(n) => query_as = query_as.bind(n),
Value::OptionalDouble(n) => query_as = query_as.bind(n),
Value::Bool(n) => query_as = query_as.bind(n),
Value::OptionalBool(n) => query_as = query_as.bind(n),
Value::Text(n) => query_as = query_as.bind(n),
Value::OptionalText(n) => query_as = query_as.bind(n),
Value::DateTime(n) => query_as = query_as.bind(n),
Value::OptionalDateTime(n) => query_as = query_as.bind(n),
}
}
let pool = crate::data::pool();
query_as.fetch_all(pool).await.unwrap_or_else(|err| {
log::error!("Unable to fetch all on query '{}': {}", query_string, err);
vec![]
})
}
}

View File

@@ -1,6 +1,7 @@
use serde::{Serialize, Deserialize};
use sqlx::Database;
use crate::data::condition::Condition;
use crate::data::executable_query::ExecutableQuery;
use crate::data::insert::InsertBuilder;
use crate::data::query::QueryBuilder;
use crate::data::update::UpdateBuilder;

View File

@@ -1,3 +1,4 @@
use crate::data::executable_query::ExecutableQuery;
use crate::data::Value;
pub struct InsertBuilder {
@@ -27,39 +28,10 @@ impl InsertBuilder {
self.returning = columns.iter().map(|s| s.to_string()).collect();
self
}
pub async fn execute(self) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
// Build the SQL query and its values
let (query_string, values) = self.build();
// Start constructing the query
let mut query = sqlx::query(&query_string);
// Bind each value to its respective placeholder
for value in values {
match value {
Value::Int(n) => query = query.bind(n),
Value::OptionalInt(n) => query = query.bind(n),
Value::BigInt(n) => query = query.bind(n),
Value::OptionalBigInt(n) => query = query.bind(n),
Value::Float(n) => query = query.bind(n),
Value::OptionalFloat(n) => query = query.bind(n),
Value::Double(n) => query = query.bind(n),
Value::OptionalDouble(n) => query = query.bind(n),
Value::Bool(n) => query = query.bind(n),
Value::OptionalBool(n) => query = query.bind(n),
Value::Text(n) => query = query.bind(n),
Value::OptionalText(n) => query = query.bind(n),
Value::DateTime(n) => query = query.bind(n),
Value::OptionalDateTime(n) => query = query.bind(n),
}
}
let pool = crate::data::pool();
query.execute(pool).await
}
fn build(self) -> (String, Vec<Value>) {
impl ExecutableQuery for InsertBuilder {
fn build(&self) -> (String, Vec<Value>) {
if self.columns.is_empty() || self.values.is_empty() {
panic!("Cannot build insert query without columns and values");
}
@@ -84,6 +56,6 @@ impl InsertBuilder {
query.push_str(&format!(" RETURNING {}", self.returning.join(", ")));
}
(query, self.values)
(query, self.values.clone())
}
}

View File

@@ -7,12 +7,13 @@ use crate::error::SirenResult;
pub mod condition;
pub mod events;
mod executable_query;
pub mod guilds;
pub mod insert;
pub mod messages;
pub mod query;
pub mod update;
mod executable_query;
pub use executable_query::ExecutableQuery;
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
static REDIS: OnceLock<RedisClient> = OnceLock::new();

View File

@@ -1,7 +1,5 @@
use std::fmt;
use std::fmt::Display;
use sqlx::{FromRow, Postgres};
use crate::data::condition::Condition;
use crate::data::executable_query::ExecutableQuery;
use crate::data::Value;
pub struct QueryBuilder {
@@ -44,45 +42,10 @@ impl QueryBuilder {
self.limit = Some(limit);
self
}
pub async fn fetch_optional<
T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>,
>(
self,
) -> Option<T> {
let (query_string, values) = self.build();
let mut query_as = sqlx::query_as(&query_string);
for value in values {
match value {
Value::Int(n) => query_as = query_as.bind(n),
Value::OptionalInt(n) => query_as = query_as.bind(n),
Value::BigInt(n) => query_as = query_as.bind(n),
Value::OptionalBigInt(n) => query_as = query_as.bind(n),
Value::Float(n) => query_as = query_as.bind(n),
Value::OptionalFloat(n) => query_as = query_as.bind(n),
Value::Double(n) => query_as = query_as.bind(n),
Value::OptionalDouble(n) => query_as = query_as.bind(n),
Value::Bool(n) => query_as = query_as.bind(n),
Value::OptionalBool(n) => query_as = query_as.bind(n),
Value::Text(n) => query_as = query_as.bind(n),
Value::OptionalText(n) => query_as = query_as.bind(n),
Value::DateTime(n) => query_as = query_as.bind(n),
Value::OptionalDateTime(n) => query_as = query_as.bind(n),
}
}
let pool = crate::data::pool();
query_as.fetch_optional(pool).await.unwrap_or_else(|err| {
log::error!(
"Unable to fetch optional on query '{}': {}",
query_string,
err
);
None
})
}
pub fn build(self) -> (String, Vec<Value>) {
impl ExecutableQuery for QueryBuilder {
fn build(&self) -> (String, Vec<Value>) {
let columns = if self.columns.is_empty() {
"*".to_string()
} else {
@@ -92,7 +55,7 @@ impl QueryBuilder {
let mut query = format!("SELECT {} FROM {}", columns, self.table);
let mut values: Vec<Value> = Vec::new();
if let Some(condition) = self.condition {
if let Some(condition) = &self.condition {
let where_condition = condition.to_sql(&mut 0);
query.push_str(&format!(" WHERE {}", where_condition.0));
values = where_condition.1;

View File

@@ -1,4 +1,5 @@
use crate::data::condition::Condition;
use crate::data::executable_query::ExecutableQuery;
use crate::data::Value;
pub struct UpdateBuilder {
@@ -28,39 +29,10 @@ impl UpdateBuilder {
self.condition = Some(condition);
self
}
pub async fn execute(self) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
// Build the SQL query and its values
let (query_string, values) = self.build();
// Start constructing the query
let mut query = sqlx::query(&query_string);
// Bind each value to its respective placeholder
for value in values {
match value {
Value::Int(n) => query = query.bind(n),
Value::OptionalInt(n) => query = query.bind(n),
Value::BigInt(n) => query = query.bind(n),
Value::OptionalBigInt(n) => query = query.bind(n),
Value::Float(n) => query = query.bind(n),
Value::OptionalFloat(n) => query = query.bind(n),
Value::Double(n) => query = query.bind(n),
Value::OptionalDouble(n) => query = query.bind(n),
Value::Bool(n) => query = query.bind(n),
Value::OptionalBool(n) => query = query.bind(n),
Value::Text(n) => query = query.bind(n),
Value::OptionalText(n) => query = query.bind(n),
Value::DateTime(n) => query = query.bind(n),
Value::OptionalDateTime(n) => query = query.bind(n),
}
}
let pool = crate::data::pool();
query.execute(pool).await
}
fn build(self) -> (String, Vec<Value>) {
impl ExecutableQuery for UpdateBuilder {
fn build(&self) -> (String, Vec<Value>) {
if self.columns.is_empty() {
panic!("Cannot build update query without columns to set");
}
@@ -76,10 +48,10 @@ impl UpdateBuilder {
let mut query = format!("UPDATE {} SET {}", self.table, set_clause);
let mut counter = self.values.len();
let mut values: Vec<Value> = self.values;
let mut values: Vec<Value> = self.values.clone();
// Build where clause
if let Some(condition) = self.condition {
if let Some(condition) = &self.condition {
let where_condition = condition.to_sql(&mut counter);
query.push_str(&format!(" WHERE {}", where_condition.0));
values.extend(where_condition.1);