From c63f3f4089c0ad07be3306fc14d4c381e9379e79 Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Mon, 10 Mar 2025 11:09:31 +0100 Subject: [PATCH] Cleanup: remove legacy query_one_row utility. --- Cargo.toml | 2 +- trailbase-core/src/admin/list_logs.rs | 15 ++- trailbase-core/src/admin/rows/list_rows.rs | 13 +-- trailbase-core/src/admin/user/list_users.rs | 17 ++- trailbase-core/src/auth/api/avatar.rs | 28 +++-- trailbase-core/src/auth/api/reset_password.rs | 16 ++- trailbase-core/src/auth/auth_test.rs | 100 ++++++++---------- trailbase-core/src/auth/oauth/callback.rs | 37 +++---- trailbase-core/src/auth/util.rs | 12 ++- trailbase-core/src/records/delete_record.rs | 65 +++++++----- trailbase-core/src/records/read_record.rs | 33 +++--- trailbase-core/src/records/test_utils.rs | 27 +++-- trailbase-core/src/records/update_record.rs | 52 +++++---- trailbase-core/src/server/init.rs | 15 +-- trailbase-core/src/table_metadata.rs | 14 +-- trailbase-core/src/util.rs | 13 --- trailbase-sqlite/src/extension.rs | 58 ++++++++++ 17 files changed, 282 insertions(+), 235 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7466b0ff..b03397dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,4 +58,4 @@ trailbase-sqlean = { path = "vendor/sqlean", version = "0.0.2" } trailbase-extension = { path = "trailbase-extension", version = "0.2.0" } trailbase-sqlite = { path = "trailbase-sqlite", version = "0.2.0" } trailbase = { path = "trailbase-core", version = "0.1.0" } -uuid = { version = "=1.12.1", default-features = false, features = ["std", "v4", "v7"] } +uuid = { version = "=1.12.1", default-features = false, features = ["std", "v4", "v7", "serde"] } diff --git a/trailbase-core/src/admin/list_logs.rs b/trailbase-core/src/admin/list_logs.rs index fa9f044e..2d207f36 100644 --- a/trailbase-core/src/admin/list_logs.rs +++ b/trailbase-core/src/admin/list_logs.rs @@ -131,19 +131,16 @@ pub async fn list_logs_handler( let table_metadata = TableMetadata::new(table.clone(), &[table]); let filter_where_clause = build_filter_where_clause(&table_metadata, filter_params)?; - let total_row_count = { - let row = crate::util::query_one_row( - conn, + let total_row_count: i64 = conn + .query_value( &format!( - "SELECT COUNT(*) FROM {LOGS_TABLE_NAME} WHERE {clause}", - clause = filter_where_clause.clause + "SELECT COUNT(*) FROM {LOGS_TABLE_NAME} WHERE {where_clause}", + where_clause = filter_where_clause.clause ), filter_where_clause.params.clone(), ) - .await?; - - row.get::(0)? - }; + .await? + .unwrap_or(-1); lazy_static! { static ref DEFAULT_ORDERING: Vec<(String, Order)> = diff --git a/trailbase-core/src/admin/rows/list_rows.rs b/trailbase-core/src/admin/rows/list_rows.rs index cd674d71..7bfdac00 100644 --- a/trailbase-core/src/admin/rows/list_rows.rs +++ b/trailbase-core/src/admin/rows/list_rows.rs @@ -65,14 +65,11 @@ pub async fn list_rows_handler( let total_row_count = { let where_clause = &filter_where_clause.clause; let count_query = format!("SELECT COUNT(*) FROM '{table_name}' WHERE {where_clause}"); - let row = crate::util::query_one_row( - state.conn(), - &count_query, - filter_where_clause.params.clone(), - ) - .await?; - - row.get::(0)? + state + .conn() + .query_value::(&count_query, filter_where_clause.params.clone()) + .await? + .unwrap_or(-1) }; let cursor_column = table_or_view_metadata.record_pk_column(); diff --git a/trailbase-core/src/admin/user/list_users.rs b/trailbase-core/src/admin/user/list_users.rs index 2d047799..ed2a8c5e 100644 --- a/trailbase-core/src/admin/user/list_users.rs +++ b/trailbase-core/src/admin/user/list_users.rs @@ -79,17 +79,16 @@ pub async fn list_users_handler( // string. let filter_where_clause = build_filter_where_clause(&*table_metadata, filter_params)?; - let total_row_count = { - let where_clause = &filter_where_clause.clause; - let row = crate::util::query_one_row( - conn, - &format!("SELECT COUNT(*) FROM {USER_TABLE} WHERE {where_clause}"), + let total_row_count: i64 = conn + .query_value( + &format!( + "SELECT COUNT(*) FROM {USER_TABLE} WHERE {where_clause}", + where_clause = filter_where_clause.clause + ), filter_where_clause.params.clone(), ) - .await?; - - row.get::(0)? - }; + .await? + .unwrap_or(-1); lazy_static! { static ref DEFAULT_ORDERING: Vec<(String, Order)> = diff --git a/trailbase-core/src/auth/api/avatar.rs b/trailbase-core/src/auth/api/avatar.rs index 87c072ed..8cce4213 100644 --- a/trailbase-core/src/auth/api/avatar.rs +++ b/trailbase-core/src/auth/api/avatar.rs @@ -20,16 +20,24 @@ async fn get_avatar_url(state: &AppState, user: &DbUser) -> Option { format!(r#"SELECT EXISTS(SELECT user FROM "{AVATAR_TABLE}" WHERE user = $1)"#); }; - if let Ok(row) = crate::util::query_one_row(state.user_conn(), &QUERY, params!(user.id)).await { - let has_avatar: bool = row.get(0).unwrap_or(false); - if has_avatar { - let site = state.site_url(); - let record_user_id = id_to_b64(&user.id); - let col_name = "file"; - return Some(format!( - "{site}/{RECORD_API_PATH}/{AVATAR_TABLE}/{record_user_id}/file/{col_name}" - )); - } + let has_avatar = state + .user_conn() + .query_value(&QUERY, params!(user.id)) + .await + .map_err(|err| { + log::debug!("avatar query broken?"); + return err; + }) + .unwrap_or_default() + .unwrap_or(false); + + if has_avatar { + let site = state.site_url(); + let record_user_id = id_to_b64(&user.id); + let col_name = "file"; + return Some(format!( + "{site}/{RECORD_API_PATH}/{AVATAR_TABLE}/{record_user_id}/file/{col_name}" + )); } return None; diff --git a/trailbase-core/src/auth/api/reset_password.rs b/trailbase-core/src/auth/api/reset_password.rs index 449c8361..e2ca6dcc 100644 --- a/trailbase-core/src/auth/api/reset_password.rs +++ b/trailbase-core/src/auth/api/reset_password.rs @@ -180,14 +180,10 @@ pub async fn force_password_reset( format!("UPDATE '{USER_TABLE}' SET password_hash = $1 WHERE email = $2 RETURNING id"); } - let id: [u8; 16] = crate::util::query_one_row( - user_conn, - &UPDATE_PASSWORD_QUERY, - params!(hashed_password, email), - ) - .await? - .get(0) - .map_err(|_err| AuthError::NotFound)?; - - return Ok(Uuid::from_bytes(id)); + return Ok( + user_conn + .query_value(&UPDATE_PASSWORD_QUERY, params!(hashed_password, email)) + .await? + .ok_or(AuthError::NotFound)?, + ); } diff --git a/trailbase-core/src/auth/auth_test.rs b/trailbase-core/src/auth/auth_test.rs index c0d63191..987fec71 100644 --- a/trailbase-core/src/auth/auth_test.rs +++ b/trailbase-core/src/auth/auth_test.rs @@ -24,7 +24,6 @@ use crate::auth::user::{DbUser, User}; use crate::constants::*; use crate::email::{testing::TestAsyncSmtpTransport, Mailer}; use crate::extract::Either; -use crate::util::query_one_row; #[tokio::test] async fn test_auth_registration_reset_and_change_email() { @@ -144,15 +143,11 @@ async fn test_auth_registration_reset_and_change_email() { .decode::(&tokens.auth_token) .unwrap(); - let session_exists: bool = query_one_row( - conn, - &session_exists_query, - (user.uuid.into_bytes().to_vec(),), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let session_exists: bool = conn + .query_value(&session_exists_query, (user.uuid.into_bytes().to_vec(),)) + .await + .unwrap() + .unwrap(); assert!(session_exists); user @@ -213,15 +208,14 @@ async fn test_auth_registration_reset_and_change_email() { assert_eq!(mailer.get_logs().len(), 2); // Steal the reset code. - let reset_code: String = query_one_row( - conn, - &format!(r#"SELECT password_reset_code FROM "{USER_TABLE}" WHERE id = $1"#), - (user.uuid.into_bytes().to_vec(),), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let reset_code: String = conn + .query_value( + &format!("SELECT password_reset_code FROM {USER_TABLE} WHERE id = $1"), + params!(user.uuid.into_bytes().to_vec()), + ) + .await + .unwrap() + .unwrap(); let reset_email_body: String = String::from_utf8_lossy( "ed_printable::decode( @@ -272,15 +266,14 @@ async fn test_auth_registration_reset_and_change_email() { .await .unwrap(); - let session_exists: bool = query_one_row( - conn, - &session_exists_query, - (user.uuid.into_bytes().to_vec(),), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let session_exists: bool = conn + .query_value( + &session_exists_query, + params!(user.uuid.into_bytes().to_vec()), + ) + .await + .unwrap() + .unwrap(); assert!(!session_exists); let tokens = login_with_password(&state, &email, &new_password) @@ -326,15 +319,14 @@ async fn test_auth_registration_reset_and_change_email() { assert_eq!(mailer.get_logs().len(), 3); // Steal the verification code. - let email_verification_code: String = query_one_row( - conn, - &format!(r#"SELECT email_verification_code FROM "{USER_TABLE}" WHERE id = $1"#), - params!(user.uuid.into_bytes()), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let email_verification_code: String = conn + .query_value( + &format!(r#"SELECT email_verification_code FROM "{USER_TABLE}" WHERE id = $1"#), + params!(user.uuid.into_bytes()), + ) + .await + .unwrap() + .unwrap(); assert!(!email_verification_code.is_empty()); let verification_email_body: String = String::from_utf8_lossy( @@ -359,15 +351,14 @@ async fn test_auth_registration_reset_and_change_email() { .await .expect(&format!("CODE: '{email_verification_code}'")); - let db_email: String = query_one_row( - conn, - &format!(r#"SELECT email FROM "{USER_TABLE}" WHERE id = $1"#), - params!(user.uuid.into_bytes()), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let db_email: String = conn + .query_value( + &format!(r#"SELECT email FROM "{USER_TABLE}" WHERE id = $1"#), + params!(user.uuid.into_bytes()), + ) + .await + .unwrap() + .unwrap(); assert_eq!(new_email, db_email); @@ -415,15 +406,14 @@ async fn test_auth_registration_reset_and_change_email() { .await .unwrap(); - let user_exists: bool = query_one_row( - conn, - &format!(r#"SELECT EXISTS(SELECT * FROM "{USER_TABLE}" WHERE id = $1)"#), - params!(user.uuid.into_bytes()), - ) - .await - .unwrap() - .get(0) - .unwrap(); + let user_exists: bool = conn + .query_value( + &format!(r#"SELECT EXISTS(SELECT * FROM "{USER_TABLE}" WHERE id = $1)"#), + params!(user.uuid.into_bytes()), + ) + .await + .unwrap() + .unwrap(); assert!(!user_exists); } diff --git a/trailbase-core/src/auth/oauth/callback.rs b/trailbase-core/src/auth/oauth/callback.rs index 1a6db99f..a6bbe58f 100644 --- a/trailbase-core/src/auth/oauth/callback.rs +++ b/trailbase-core/src/auth/oauth/callback.rs @@ -9,6 +9,7 @@ use oauth2::{AuthorizationCode, StandardTokenResponse, TokenResponse}; use serde::Deserialize; use tower_cookies::Cookies; use trailbase_sqlite::{named_params, params}; +use uuid::Uuid; use crate::auth::oauth::state::{OAuthState, ResponseType}; use crate::auth::oauth::OAuthUser; @@ -206,7 +207,7 @@ pub(crate) async fn callback_from_external_auth_provider( async fn create_user_for_external_provider( conn: &trailbase_sqlite::Connection, user: &OAuthUser, -) -> Result { +) -> Result { if !user.verified { return Err(AuthError::Unauthorized); } @@ -223,24 +224,21 @@ async fn create_user_for_external_provider( ); } - let row = crate::util::query_one_row( - conn, - &QUERY, - named_params! { - ":provider_id": user.provider_id as i64, - ":provider_user_id": user.provider_user_id.clone(), - ":verified": user.verified as i64, - ":email": user.email.clone(), - ":avatar": user.avatar.clone(), - }, - ) - .await?; + let id: Uuid = conn + .query_value( + &QUERY, + named_params! { + ":provider_id": user.provider_id as i64, + ":provider_user_id": user.provider_user_id.clone(), + ":verified": user.verified as i64, + ":email": user.email.clone(), + ":avatar": user.avatar.clone(), + }, + ) + .await? + .ok_or_else(|| AuthError::Internal("query should return".into()))?; - return Ok(uuid::Uuid::from_bytes( - row - .get::<[u8; 16]>(0) - .map_err(|err| AuthError::Internal(err.into()))?, - )); + return Ok(id); } async fn user_by_provider_id( @@ -258,7 +256,6 @@ async fn user_by_provider_id( &QUERY, params!(provider_id as i64, provider_user_id.to_string()), ) - .await - .map_err(|err| AuthError::Internal(err.into()))? + .await? .ok_or_else(|| AuthError::NotFound); } diff --git a/trailbase-core/src/auth/util.rs b/trailbase-core/src/auth/util.rs index e0474d0e..f50f4f68 100644 --- a/trailbase-core/src/auth/util.rs +++ b/trailbase-core/src/auth/util.rs @@ -150,11 +150,13 @@ pub async fn user_exists(state: &AppState, email: &str) -> Result(0) - .map_err(|err| AuthError::Internal(err.into())); + return Ok( + state + .user_conn() + .query_value(&QUERY, params!(email.to_string())) + .await? + .ok_or_else(|| AuthError::Internal("query should return".into()))?, + ); } pub(crate) async fn is_admin(state: &AppState, user: &User) -> bool { diff --git a/trailbase-core/src/records/delete_record.rs b/trailbase-core/src/records/delete_record.rs index 286cd275..939f3533 100644 --- a/trailbase-core/src/records/delete_record.rs +++ b/trailbase-core/src/records/delete_record.rs @@ -69,12 +69,12 @@ mod test { use crate::util::{b64_to_id, id_to_b64}; #[tokio::test] - async fn test_record_api_delete() -> Result<(), anyhow::Error> { - let state = test_state(None).await?; + async fn test_record_api_delete() { + let state = test_state(None).await.unwrap(); let conn = state.conn(); - create_chat_message_app_tables(&state).await?; - let room = add_room(conn, "room0").await?; + create_chat_message_app_tables(&state).await.unwrap(); + let room = add_room(conn, "room0").await.unwrap(); let password = "Secret!1!!"; // Register message table as api with moderator read access. @@ -100,54 +100,61 @@ mod test { ..Default::default() }, ) - .await?; + .await + .unwrap(); let user_x_email = "user_x@test.com"; let user_x = create_user_for_test(&state, user_x_email, password) - .await? + .await + .unwrap() .into_bytes(); - let user_x_token = login_with_password(&state, user_x_email, password).await?; + let user_x_token = login_with_password(&state, user_x_email, password) + .await + .unwrap(); - add_user_to_room(conn, user_x, room).await?; + add_user_to_room(conn, user_x, room).await.unwrap(); let user_y_email = "user_y@foo.baz"; let _user_y = create_user_for_test(&state, user_y_email, password) - .await? + .await + .unwrap() .into_bytes(); - let user_y_token = login_with_password(&state, user_y_email, password).await?; + let user_y_token = login_with_password(&state, user_y_email, password) + .await + .unwrap(); { // User X can delete their own message. - let id = add_message(&state, &user_x, &user_x_token.auth_token, &room).await?; - delete_message(&state, &user_x_token.auth_token, &id).await?; - assert_eq!(message_exists(conn, &id).await?, false); + let id = add_message(&state, &user_x, &user_x_token.auth_token, &room) + .await + .unwrap(); + delete_message(&state, &user_x_token.auth_token, &id) + .await + .unwrap(); + assert_eq!(message_exists(conn, &id).await, false); } { // User Y cannot delete X's message. - let id = add_message(&state, &user_x, &user_x_token.auth_token, &room).await?; + let id = add_message(&state, &user_x, &user_x_token.auth_token, &room) + .await + .unwrap(); let response = delete_message(&state, &user_y_token.auth_token, &id).await; assert!(response.is_err()); - assert_eq!(message_exists(conn, &id).await?, true); + assert_eq!(message_exists(conn, &id).await, true); } - - return Ok(()); } - async fn message_exists( - conn: &trailbase_sqlite::Connection, - id: &[u8; 16], - ) -> Result { - let count: i64 = crate::util::query_one_row( - conn, - "SELECT COUNT(*) FROM message WHERE id = $1", - params!(*id), - ) - .await? - .get(0)?; - return Ok(count > 0); + async fn message_exists(conn: &trailbase_sqlite::Connection, id: &[u8; 16]) -> bool { + let count: i64 = conn + .query_value("SELECT COUNT(*) FROM message WHERE id = $1", params!(*id)) + .await + .unwrap() + .unwrap(); + + return count > 0; } async fn add_message( diff --git a/trailbase-core/src/records/read_record.rs b/trailbase-core/src/records/read_record.rs index 511afdc8..432c5488 100644 --- a/trailbase-core/src/records/read_record.rs +++ b/trailbase-core/src/records/read_record.rs @@ -251,14 +251,14 @@ mod test { use crate::records::test_utils::*; use crate::records::*; use crate::test::unpack_json_response; - use crate::util::{id_to_b64, query_one_row}; + use crate::util::id_to_b64; #[tokio::test] - async fn ignores_extra_sql_parameters_test() -> Result<(), anyhow::Error> { + async fn ignores_extra_sql_parameters_test() { // This test is actually just testing our SQL driver and making sure that we can overprovision // arguments. Specifically, we want to provide :user and :id arguments even if they're not // consumed by a user-provided access query. - let state = test_state(None).await?; + let state = test_state(None).await.unwrap(); let conn = state.user_conn(); const EMAIL: &str = "foo@bar.baz"; @@ -267,20 +267,21 @@ mod test { &format!(r#"INSERT INTO "{USER_TABLE}" (email) VALUES ($1)"#), trailbase_sqlite::params!(EMAIL), ) - .await?; + .await + .unwrap(); - query_one_row( - conn, - &format!(r#"SELECT * from "{USER_TABLE}" WHERE email = :email"#), - trailbase_sqlite::named_params! { - ":email": EMAIL, - ":unused": "unused", - ":foo": 42, - }, - ) - .await?; - - return Ok(()); + conn + .query_row( + &format!(r#"SELECT * from "{USER_TABLE}" WHERE email = :email"#), + trailbase_sqlite::named_params! { + ":email": EMAIL, + ":unused": "unused", + ":foo": 42, + }, + ) + .await + .unwrap() + .unwrap(); } #[tokio::test] diff --git a/trailbase-core/src/records/test_utils.rs b/trailbase-core/src/records/test_utils.rs index 25de8d84..c0378742 100644 --- a/trailbase-core/src/records/test_utils.rs +++ b/trailbase-core/src/records/test_utils.rs @@ -3,7 +3,6 @@ mod tests { use trailbase_sqlite::params; use crate::records::json_to_sql::JsonRow; - use crate::util::query_one_row; use crate::AppState; pub async fn create_chat_message_app_tables(state: &AppState) -> Result<(), anyhow::Error> { @@ -90,15 +89,15 @@ mod tests { conn: &trailbase_sqlite::Connection, name: &str, ) -> Result<[u8; 16], anyhow::Error> { - let room: [u8; 16] = query_one_row( - conn, - "INSERT INTO room (name) VALUES ($1) RETURNING id", - params!(name.to_string()), - ) - .await? - .get(0)?; + let room: uuid::Uuid = conn + .query_value( + "INSERT INTO room (name) VALUES ($1) RETURNING id", + params!(name.to_string()), + ) + .await? + .ok_or(rusqlite::Error::QueryReturnedNoRows)?; - return Ok(room); + return Ok(room.into_bytes()); } pub async fn add_user_to_room( @@ -121,15 +120,15 @@ mod tests { room: [u8; 16], message: &str, ) -> Result<[u8; 16], anyhow::Error> { - return Ok( - query_one_row( - conn, + let id: uuid::Uuid = conn + .query_value( "INSERT INTO message (_owner, room, data) VALUES ($1, $2, $3) RETURNING id", params!(user, room, message.to_string()), ) .await? - .get(0)?, - ); + .ok_or(rusqlite::Error::QueryReturnedNoRows)?; + + return Ok(id.into_bytes()); } pub fn json_row_from_value(value: serde_json::Value) -> Result { diff --git a/trailbase-core/src/records/update_record.rs b/trailbase-core/src/records/update_record.rs index ed9ee13d..0489dfc3 100644 --- a/trailbase-core/src/records/update_record.rs +++ b/trailbase-core/src/records/update_record.rs @@ -80,15 +80,15 @@ mod test { use crate::records::test_utils::*; use crate::records::*; use crate::test::unpack_json_response; - use crate::util::{b64_to_id, id_to_b64, query_one_row}; + use crate::util::{b64_to_id, id_to_b64}; #[tokio::test] - async fn test_record_api_update() -> Result<(), anyhow::Error> { - let state = test_state(None).await?; + async fn test_record_api_update() { + let state = test_state(None).await.unwrap(); let conn = state.conn(); - create_chat_message_app_tables(&state).await?; - let room = add_room(conn, "room0").await?; + create_chat_message_app_tables(&state).await.unwrap(); + let room = add_room(conn, "room0").await.unwrap(); let password = "Secret!1!!"; // Register message table and api with moderator read access. @@ -116,23 +116,30 @@ mod test { ..Default::default() }, ) - .await?; + .await + .unwrap(); let user_x_email = "user_x@test.com"; let user_x = create_user_for_test(&state, user_x_email, password) - .await? + .await + .unwrap() .into_bytes(); - let user_x_token = login_with_password(&state, user_x_email, password).await?; + let user_x_token = login_with_password(&state, user_x_email, password) + .await + .unwrap(); - add_user_to_room(conn, user_x, room).await?; + add_user_to_room(conn, user_x, room).await.unwrap(); let user_y_email = "user_y@foo.baz"; let _user_y = create_user_for_test(&state, user_y_email, password) - .await? + .await + .unwrap() .into_bytes(); - let user_y_token = login_with_password(&state, user_y_email, password).await?; + let user_y_token = login_with_password(&state, user_y_email, password) + .await + .unwrap(); let create_json = serde_json::json!({ "_owner": id_to_b64(&user_x), @@ -147,9 +154,11 @@ mod test { User::from_auth_token(&state, &user_x_token.auth_token), Either::Json(json_row_from_value(create_json).unwrap().into()), ) - .await?, + .await + .unwrap(), ) - .await?; + .await + .unwrap(); assert_eq!(create_response.ids.len(), 1); let b64_id = create_response.ids[0].clone(); @@ -170,13 +179,14 @@ mod test { assert!(update_response.is_ok(), "{b64_id} {update_response:?}"); - let message_text: String = query_one_row( - conn, - "SELECT data FROM message WHERE id = $1", - params!(b64_to_id(&b64_id)?), - ) - .await? - .get(0)?; + let message_text: String = conn + .query_value( + "SELECT data FROM message WHERE id = $1", + params!(b64_to_id(&b64_id).unwrap()), + ) + .await + .unwrap() + .unwrap(); assert_eq!(updated_message_text, message_text); } @@ -195,7 +205,5 @@ mod test { assert!(update_response.is_err(), "{b64_id} {update_response:?}"); } - - return Ok(()); } } diff --git a/trailbase-core/src/server/init.rs b/trailbase-core/src/server/init.rs index fcada7e6..fb8d2983 100644 --- a/trailbase-core/src/server/init.rs +++ b/trailbase-core/src/server/init.rs @@ -137,13 +137,14 @@ pub async fn init_app_state( }); if new_db { - let num_admins: i64 = crate::util::query_one_row( - app_state.user_conn(), - &format!("SELECT COUNT(*) FROM {USER_TABLE} WHERE admin = TRUE"), - (), - ) - .await? - .get(0)?; + let num_admins: i64 = app_state + .user_conn() + .query_value( + &format!("SELECT COUNT(*) FROM {USER_TABLE} WHERE admin = TRUE"), + (), + ) + .await? + .unwrap_or(0); if num_admins == 0 { let email = "admin@localhost".to_string(); diff --git a/trailbase-core/src/table_metadata.rs b/trailbase-core/src/table_metadata.rs index d3141b3c..03575cb3 100644 --- a/trailbase-core/src/table_metadata.rs +++ b/trailbase-core/src/table_metadata.rs @@ -560,13 +560,13 @@ pub async fn lookup_and_parse_table_schema( table_name: &str, ) -> Result { // Then get the actual table. - let sql: String = crate::util::query_one_row( - conn, - &format!("SELECT sql FROM {SQLITE_SCHEMA_TABLE} WHERE type = 'table' AND name = $1"), - params!(table_name.to_string()), - ) - .await? - .get(0)?; + let sql: String = conn + .query_value( + &format!("SELECT sql FROM {SQLITE_SCHEMA_TABLE} WHERE type = 'table' AND name = $1"), + params!(table_name.to_string()), + ) + .await? + .ok_or_else(|| trailbase_sqlite::Error::Rusqlite(rusqlite::Error::QueryReturnedNoRows))?; let Some(stmt) = sqlite3_parse_into_statement(&sql)? else { return Err(TableLookupError::Missing); diff --git a/trailbase-core/src/util.rs b/trailbase-core/src/util.rs index 9bf81f04..9ae1bba5 100644 --- a/trailbase-core/src/util.rs +++ b/trailbase-core/src/util.rs @@ -58,19 +58,6 @@ pub(crate) fn assert_uuidv7_version(uuid: &Uuid) { #[cfg(not(debug_assertions))] pub(crate) fn assert_uuidv7_version(_uuid: &Uuid) {} -pub async fn query_one_row( - conn: &trailbase_sqlite::Connection, - sql: &str, - params: impl trailbase_sqlite::Params + Send + 'static, -) -> Result { - if let Some(row) = conn.query_row(sql, params).await? { - return Ok(row); - } - return Err(trailbase_sqlite::Error::Rusqlite( - rusqlite::Error::QueryReturnedNoRows, - )); -} - #[inline] pub(crate) fn get_header(headers: &HeaderMap, header_name: impl AsHeaderName) -> Option<&str> { if let Some(header) = headers.get(header_name) { diff --git a/trailbase-sqlite/src/extension.rs b/trailbase-sqlite/src/extension.rs index a2199c85..94b60bd5 100644 --- a/trailbase-sqlite/src/extension.rs +++ b/trailbase-sqlite/src/extension.rs @@ -128,4 +128,62 @@ mod test { .query_row("SELECT vec_f32('[0, 1, 2, 3]')", (), |_row| Ok(())) .unwrap(); } + + #[tokio::test] + async fn test_uuids() { + let conn = crate::Connection::from_conn(connect_sqlite(None, None).unwrap()).unwrap(); + + conn + .execute( + r#"CREATE TABLE test ( + id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid_v7(id)) DEFAULT(uuid_v7()), + text TEXT + )"#, + (), + ) + .await + .unwrap(); + + // V4 fails + assert!(conn + .execute( + "INSERT INTO test (id) VALUES (?1) ", + crate::params!(uuid::Uuid::new_v4().into_bytes()) + ) + .await + .is_err()); + + // V7 succeeds + let id = uuid::Uuid::now_v7(); + assert!(conn + .execute( + "INSERT INTO test (id) VALUES (?1) ", + crate::params!(id.into_bytes()) + ) + .await + .is_ok()); + + let read_id: uuid::Uuid = conn + .query_value("SELECT id FROM test LIMIT 1", ()) + .await + .unwrap() + .unwrap(); + + assert_eq!(id, read_id); + + let blob: Vec = conn + .query_value("SELECT id FROM test LIMIT 1", ()) + .await + .unwrap() + .unwrap(); + + assert_eq!(id, Uuid::from_slice(&blob).unwrap()); + + let arr = conn + .query_value::<[u8; 16]>("SELECT id FROM test LIMIT 1", ()) + .await; + + // FIXME: serde_rusqlite doesn't seem to be able to serialize blobs into [u8; N]. + assert!(arr.is_err()); + } }