Cleanup: remove legacy query_one_row utility.

This commit is contained in:
Sebastian Jeltsch
2025-03-10 11:09:31 +01:00
parent bbd747a4ab
commit c63f3f4089
17 changed files with 282 additions and 235 deletions

View File

@@ -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"] }

View File

@@ -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::<i64>(0)?
};
.await?
.unwrap_or(-1);
lazy_static! {
static ref DEFAULT_ORDERING: Vec<(String, Order)> =

View File

@@ -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::<i64>(0)?
state
.conn()
.query_value::<i64>(&count_query, filter_where_clause.params.clone())
.await?
.unwrap_or(-1)
};
let cursor_column = table_or_view_metadata.record_pk_column();

View File

@@ -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::<i64>(0)?
};
.await?
.unwrap_or(-1);
lazy_static! {
static ref DEFAULT_ORDERING: Vec<(String, Order)> =

View File

@@ -20,16 +20,24 @@ async fn get_avatar_url(state: &AppState, user: &DbUser) -> Option<String> {
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;

View File

@@ -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)?,
);
}

View File

@@ -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::<TokenClaims>(&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(
&quoted_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);
}

View File

@@ -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<uuid::Uuid, AuthError> {
) -> Result<Uuid, AuthError> {
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);
}

View File

@@ -150,11 +150,13 @@ pub async fn user_exists(state: &AppState, email: &str) -> Result<bool, AuthErro
static ref QUERY: String =
format!(r#"SELECT EXISTS(SELECT 1 FROM "{USER_TABLE}" WHERE email = $1)"#);
};
let row =
crate::util::query_one_row(state.user_conn(), &QUERY, params!(email.to_string())).await?;
return row
.get::<bool>(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 {

View File

@@ -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<bool, anyhow::Error> {
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(

View File

@@ -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]

View File

@@ -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<JsonRow, anyhow::Error> {

View File

@@ -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(());
}
}

View File

@@ -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();

View File

@@ -560,13 +560,13 @@ pub async fn lookup_and_parse_table_schema(
table_name: &str,
) -> Result<Table, TableLookupError> {
// 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);

View File

@@ -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<trailbase_sqlite::Row, trailbase_sqlite::Error> {
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) {

View File

@@ -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<u8> = 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());
}
}