fix(sql macro): permit code to compile

This commit is contained in:
Edouard Vanbelle
2026-06-30 19:46:07 +02:00
parent 08f2175455
commit 5631b7e062
2 changed files with 65 additions and 39 deletions
@@ -3,10 +3,18 @@
//! RFC 4918 §4.2 defines "dead properties" as those stored verbatim by the //! RFC 4918 §4.2 defines "dead properties" as those stored verbatim by the
//! server without interpreting their value. Properties are persisted to //! server without interpreting their value. Properties are persisted to
//! `storage.webdav_dead_properties` and survive server restarts. //! `storage.webdav_dead_properties` and survive server restarts.
//!
//! Queries here use `sqlx::query()` (runtime-bound) rather than the
//! compile-time-checked `sqlx::query!()` macro. The macro would require either
//! a live DB at compile time OR committed `.sqlx/` offline metadata; the rest
//! of this codebase consistently uses the runtime variant (see
//! `user_pg_repository.rs` for the canonical style), so a fresh checkout
//! compiles without any DB connection. Trading the macro's compile-time column
//! check for that bootstrap-friendliness is the project's standing convention.
use std::sync::Arc; use std::sync::Arc;
use sqlx::PgPool; use sqlx::{PgPool, Row};
use uuid::Uuid; use uuid::Uuid;
use crate::application::adapters::webdav_adapter::QualifiedName; use crate::application::adapters::webdav_adapter::QualifiedName;
@@ -29,7 +37,7 @@ impl DeadPropertyStore {
name: QualifiedName, name: QualifiedName,
value: Option<String>, value: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
sqlx::query!( sqlx::query(
r#" r#"
INSERT INTO storage.webdav_dead_properties INSERT INTO storage.webdav_dead_properties
(resource_path, user_id, namespace, local_name, value) (resource_path, user_id, namespace, local_name, value)
@@ -37,12 +45,12 @@ impl DeadPropertyStore {
ON CONFLICT (resource_path, user_id, namespace, local_name) ON CONFLICT (resource_path, user_id, namespace, local_name)
DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP
"#, "#,
path,
user_id,
name.namespace,
name.name,
value,
) )
.bind(path)
.bind(user_id)
.bind(&name.namespace)
.bind(&name.name)
.bind(&value)
.execute(&*self.pool) .execute(&*self.pool)
.await .await
.map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("set: {e}")))?; .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("set: {e}")))?;
@@ -56,15 +64,15 @@ impl DeadPropertyStore {
user_id: Uuid, user_id: Uuid,
name: &QualifiedName, name: &QualifiedName,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
sqlx::query!( sqlx::query(
"DELETE FROM storage.webdav_dead_properties "DELETE FROM storage.webdav_dead_properties
WHERE resource_path = $1 AND user_id = $2 WHERE resource_path = $1 AND user_id = $2
AND namespace = $3 AND local_name = $4", AND namespace = $3 AND local_name = $4",
path,
user_id,
name.namespace,
name.name,
) )
.bind(path)
.bind(user_id)
.bind(&name.namespace)
.bind(&name.name)
.execute(&*self.pool) .execute(&*self.pool)
.await .await
.map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("remove: {e}")))?; .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("remove: {e}")))?;
@@ -77,20 +85,25 @@ impl DeadPropertyStore {
path: &str, path: &str,
user_id: Uuid, user_id: Uuid,
) -> Result<Vec<(QualifiedName, Option<String>)>, DomainError> { ) -> Result<Vec<(QualifiedName, Option<String>)>, DomainError> {
let rows = sqlx::query!( let rows = sqlx::query(
"SELECT namespace, local_name, value "SELECT namespace, local_name, value
FROM storage.webdav_dead_properties FROM storage.webdav_dead_properties
WHERE resource_path = $1 AND user_id = $2", WHERE resource_path = $1 AND user_id = $2",
path,
user_id,
) )
.bind(path)
.bind(user_id)
.fetch_all(&*self.pool) .fetch_all(&*self.pool)
.await .await
.map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get_all: {e}")))?; .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get_all: {e}")))?;
Ok(rows Ok(rows
.into_iter() .into_iter()
.map(|r| (QualifiedName::new(r.namespace, r.local_name), r.value)) .map(|r| {
let namespace: String = r.get("namespace");
let local_name: String = r.get("local_name");
let value: Option<String> = r.get("value");
(QualifiedName::new(namespace, local_name), value)
})
.collect()) .collect())
} }
@@ -102,30 +115,30 @@ impl DeadPropertyStore {
user_id: Uuid, user_id: Uuid,
name: &QualifiedName, name: &QualifiedName,
) -> Result<Option<Option<String>>, DomainError> { ) -> Result<Option<Option<String>>, DomainError> {
let row = sqlx::query!( let row = sqlx::query(
"SELECT value FROM storage.webdav_dead_properties "SELECT value FROM storage.webdav_dead_properties
WHERE resource_path = $1 AND user_id = $2 WHERE resource_path = $1 AND user_id = $2
AND namespace = $3 AND local_name = $4", AND namespace = $3 AND local_name = $4",
path,
user_id,
name.namespace,
name.name,
) )
.bind(path)
.bind(user_id)
.bind(&name.namespace)
.bind(&name.name)
.fetch_optional(&*self.pool) .fetch_optional(&*self.pool)
.await .await
.map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get: {e}")))?; .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get: {e}")))?;
Ok(row.map(|r| r.value)) Ok(row.map(|r| r.get::<Option<String>, _>("value")))
} }
/// Delete all dead properties for `path` (called on DELETE). /// Delete all dead properties for `path` (called on DELETE).
pub async fn remove_resource(&self, path: &str, user_id: Uuid) -> Result<(), DomainError> { pub async fn remove_resource(&self, path: &str, user_id: Uuid) -> Result<(), DomainError> {
sqlx::query!( sqlx::query(
"DELETE FROM storage.webdav_dead_properties "DELETE FROM storage.webdav_dead_properties
WHERE resource_path = $1 AND user_id = $2", WHERE resource_path = $1 AND user_id = $2",
path,
user_id,
) )
.bind(path)
.bind(user_id)
.execute(&*self.pool) .execute(&*self.pool)
.await .await
.map_err(|e| { .map_err(|e| {
@@ -146,26 +159,26 @@ impl DeadPropertyStore {
DomainError::internal_error("DeadPropertyStore", format!("rename_resource tx: {e}")) DomainError::internal_error("DeadPropertyStore", format!("rename_resource tx: {e}"))
})?; })?;
sqlx::query!( sqlx::query(
"DELETE FROM storage.webdav_dead_properties "DELETE FROM storage.webdav_dead_properties
WHERE resource_path = $1 AND user_id = $2", WHERE resource_path = $1 AND user_id = $2",
new_path,
user_id,
) )
.bind(new_path)
.bind(user_id)
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|e| { .map_err(|e| {
DomainError::internal_error("DeadPropertyStore", format!("rename_resource delete: {e}")) DomainError::internal_error("DeadPropertyStore", format!("rename_resource delete: {e}"))
})?; })?;
sqlx::query!( sqlx::query(
"UPDATE storage.webdav_dead_properties "UPDATE storage.webdav_dead_properties
SET resource_path = $2 SET resource_path = $2
WHERE resource_path = $1 AND user_id = $3", WHERE resource_path = $1 AND user_id = $3",
old_path,
new_path,
user_id,
) )
.bind(old_path)
.bind(new_path)
.bind(user_id)
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|e| { .map_err(|e| {
+20 -7
View File
@@ -487,7 +487,10 @@ async fn handle_propfind(
.await; .await;
} }
Ok(ResolvedResource::File(file)) => { Ok(ResolvedResource::File(file)) => {
let dead_props = state.webdav_dead_props.get_all(&path, user.id).await let dead_props = state
.webdav_dead_props
.get_all(&path, user.id)
.await
.unwrap_or_default(); .unwrap_or_default();
let file_href = webdav_href(&client_path); let file_href = webdav_href(&client_path);
let mut buf = Vec::with_capacity(1024); let mut buf = Vec::with_capacity(1024);
@@ -541,7 +544,10 @@ async fn handle_propfind(
.await .await
{ {
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?; assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
let dead_props = state.webdav_dead_props.get_all(&path, user.id).await let dead_props = state
.webdav_dead_props
.get_all(&path, user.id)
.await
.unwrap_or_default(); .unwrap_or_default();
let file_href = webdav_href(&client_path); let file_href = webdav_href(&client_path);
let mut buf = Vec::with_capacity(1024); let mut buf = Vec::with_capacity(1024);
@@ -785,13 +791,18 @@ async fn handle_proppatch(
for op in &ops { for op in &ops {
match op { match op {
PropPatchOp::Set(pv) => { PropPatchOp::Set(pv) => {
dead_props.set(&path, user.id, pv.name.clone(), pv.value.clone()).await dead_props
.map_err(|e| AppError::internal_error(format!("Failed to store dead property: {e}")))?; .set(&path, user.id, pv.name.clone(), pv.value.clone())
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to store dead property: {e}"))
})?;
results.push((&pv.name, true)); results.push((&pv.name, true));
} }
PropPatchOp::Remove(name) => { PropPatchOp::Remove(name) => {
dead_props.remove(&path, user.id, name).await dead_props.remove(&path, user.id, name).await.map_err(|e| {
.map_err(|e| AppError::internal_error(format!("Failed to remove dead property: {e}")))?; AppError::internal_error(format!("Failed to remove dead property: {e}"))
})?;
results.push((name, true)); results.push((name, true));
} }
} }
@@ -1112,7 +1123,9 @@ fn enforce_native_lock(
if p.is_empty() { if p.is_empty() {
return None; return None;
} }
if let Some(e) = lock_store.get_by_path(p) && e.info.depth.eq_ignore_ascii_case("infinity") { if let Some(e) = lock_store.get_by_path(p)
&& e.info.depth.eq_ignore_ascii_case("infinity")
{
return Some(e); return Some(e);
} }
} }