a lot of things
ci / docker (backend, backend/Dockerfile, backend) (push) Failing after 9m32s
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Successful in 9m48s

This commit is contained in:
2025-10-14 00:56:25 +02:00
parent 9060905e7a
commit c17a9484ca
18 changed files with 3207 additions and 483 deletions
+2 -6
View File
@@ -53,7 +53,7 @@ pub struct Document {
pub original_name: String,
pub content_type: Option<String>,
pub folder_id: Option<Uuid>,
pub current_version: i32,
pub current_version_id: Uuid,
pub uploaded_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub deleted_at: Option<NaiveDateTime>,
@@ -70,7 +70,7 @@ pub struct NewDocument {
pub original_name: String,
pub content_type: Option<String>,
pub folder_id: Option<Uuid>,
pub current_version: i32,
pub current_version_id: Uuid,
pub metadata: serde_json::Value,
pub issued_at: Option<NaiveDateTime>,
pub title: String,
@@ -111,8 +111,6 @@ pub struct DocumentAsset {
pub asset_type: String,
pub s3_key: String,
pub mime_type: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub metadata: serde_json::Value,
pub created_at: NaiveDateTime,
}
@@ -125,8 +123,6 @@ pub struct NewDocumentAsset {
pub asset_type: String,
pub s3_key: String,
pub mime_type: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub metadata: serde_json::Value,
}
+133 -228
View File
@@ -77,40 +77,6 @@ impl From<Tag> for TagResponse {
}
}
#[derive(Serialize, Clone, Default)]
pub struct DocumentAssetGroupResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<DocumentAssetResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<DocumentAssetResponse>,
}
impl DocumentAssetGroupResponse {
pub fn is_empty(&self) -> bool {
self.thumbnail.is_none() && self.preview.is_none()
}
}
#[derive(Serialize)]
pub struct DocumentResponse {
pub id: Uuid,
pub filename: String,
pub title: String,
pub original_name: String,
pub content_type: Option<String>,
pub folder_id: Option<Uuid>,
pub current_version: i32,
pub uploaded_at: String,
pub updated_at: String,
pub deleted_at: Option<String>,
pub issued_at: Option<String>,
pub metadata: Value,
pub tags: Vec<TagResponse>,
#[serde(default, skip_serializing_if = "DocumentAssetGroupResponse::is_empty")]
pub assets: DocumentAssetGroupResponse,
pub download_path: String,
}
#[derive(Serialize, Clone)]
pub struct DocumentVersionResponse {
pub id: Uuid,
@@ -127,19 +93,41 @@ pub struct DocumentAssetResponse {
pub id: Uuid,
pub asset_type: String,
pub mime_type: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub url: String,
pub metadata: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub created_at: String,
}
type PrimaryDocumentAssets = DocumentAssetGroupResponse;
#[derive(Serialize, Clone)]
pub struct DocumentCurrentVersionResponse {
#[serde(flatten)]
pub version: DocumentVersionResponse,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assets: Vec<DocumentAssetResponse>,
pub download_path: String,
}
#[derive(Serialize)]
pub struct DocumentResponse {
pub id: Uuid,
pub filename: String,
pub title: String,
pub original_name: String,
pub content_type: Option<String>,
pub folder_id: Option<Uuid>,
pub uploaded_at: String,
pub updated_at: String,
pub deleted_at: Option<String>,
pub issued_at: Option<String>,
pub metadata: Value,
pub tags: Vec<TagResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_version: Option<DocumentCurrentVersionResponse>,
}
#[derive(Serialize)]
pub struct DocumentDetailResponse {
pub document: DocumentResponse,
pub current_version: DocumentVersionResponse,
pub assets: Vec<DocumentAssetResponse>,
}
#[derive(Serialize)]
@@ -256,18 +244,18 @@ pub async fn list_documents(
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
drop(conn);
let primary_assets = load_primary_assets(&state, &docs).await?;
let primary_versions = load_primary_assets(&state, &docs).await?;
let mut response = Vec::with_capacity(doc_ids.len());
for doc in docs {
let tags = tags_map.get(&doc.id).cloned();
let assets = primary_assets.get(&doc.id).cloned();
let current_version = primary_versions.get(&doc.id).cloned();
response.push(to_document_response(
&state,
user.user_id,
doc,
tags,
assets,
current_version,
)?);
}
@@ -287,8 +275,7 @@ pub async fn get_document(
}
let current_version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::version_number.eq(doc.current_version))
.find(doc.current_version_id)
.first(&mut conn)?;
let tags_map = load_tags_for_documents(&mut conn, &[document_id])?;
@@ -296,21 +283,7 @@ pub async fn get_document(
drop(conn);
let assets = load_asset_responses(&state, version_id).await?;
let mut grouped_assets = DocumentAssetGroupResponse::default();
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned()
{
grouped_assets.thumbnail = Some(asset);
}
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned()
{
grouped_assets.preview = Some(asset);
}
let version_response = to_version_response(current_version);
Ok(Json(DocumentDetailResponse {
document: to_document_response(
@@ -318,10 +291,8 @@ pub async fn get_document(
user.user_id,
doc,
tags_map.get(&document_id).cloned(),
Some(grouped_assets),
Some((version_response, assets)),
)?,
current_version: to_version_response(current_version),
assets,
}))
}
@@ -441,17 +412,12 @@ pub async fn request_document_assets(
return Err(AppError::not_found());
}
let version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::version_number.eq(document.current_version))
.first(&mut conn)?;
enqueue_job(
&mut conn,
JOB_ANALYZE_DOCUMENT,
json!({
"document_id": document_id,
"document_version_id": version.id,
"document_version_id": document.current_version_id,
"force": query.force,
}),
None,
@@ -467,11 +433,9 @@ pub async fn reanalyze_all_documents(
) -> AppResult<(StatusCode, Json<BulkReanalyzeResponse>)> {
let mut conn = state.db()?;
let targets: Vec<(Uuid, Uuid)> = document_versions::table
.inner_join(documents::table.on(document_versions::document_id.eq(documents::id)))
let targets: Vec<(Uuid, Uuid)> = documents::table
.filter(documents::deleted_at.is_null())
.filter(document_versions::version_number.eq(documents::current_version))
.select((documents::id, document_versions::id))
.select((documents::id, documents::current_version_id))
.load(&mut conn)?;
let mut queued = 0usize;
@@ -511,12 +475,10 @@ pub async fn reanalyze_selected_documents(
let mut conn = state.db()?;
let targets: Vec<(Uuid, Uuid)> = document_versions::table
.inner_join(documents::table.on(document_versions::document_id.eq(documents::id)))
let targets: Vec<(Uuid, Uuid)> = documents::table
.filter(documents::id.eq_any(&document_ids))
.filter(documents::deleted_at.is_null())
.filter(document_versions::version_number.eq(documents::current_version))
.select((documents::id, document_versions::id))
.select((documents::id, documents::current_version_id))
.load(&mut conn)?;
if targets.len() != document_ids.len() {
@@ -554,16 +516,44 @@ pub async fn list_document_assets(
return Err(AppError::not_found());
}
let version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::version_number.eq(document.current_version))
.first(&mut conn)?;
let version_id = document.current_version_id;
drop(conn);
let assets = load_asset_responses(&state, version.id).await?;
let assets = load_asset_responses(&state, version_id).await?;
Ok(Json(assets))
}
pub async fn get_document_asset(
State(state): State<AppState>,
Path((document_id, asset_id)): Path<(Uuid, Uuid)>,
) -> AppResult<Json<DocumentAssetResponse>> {
let mut conn = state.db()?;
let document: Document = documents::table.find(document_id).first(&mut conn)?;
if document.deleted_at.is_some() {
return Err(AppError::not_found());
}
let asset: DocumentAsset = document_assets::table.find(asset_id).first(&mut conn)?;
let version: DocumentVersion = document_versions::table
.find(asset.document_version_id)
.first(&mut conn)?;
if version.document_id != document_id {
return Err(AppError::not_found());
}
let s3_key = asset.s3_key.clone();
drop(conn);
let presigned_url = state
.storage
.presign_get_object(&s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.await
.map_err(|err| AppError::internal(format!("failed to generate asset URL: {err}")))?;
Ok(Json(to_asset_response(asset, Some(presigned_url))))
}
pub async fn download_document(
State(state): State<AppState>,
Path(document_id): Path<Uuid>,
@@ -575,8 +565,7 @@ pub async fn download_document(
}
let version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::version_number.eq(doc.current_version))
.find(doc.current_version_id)
.first(&mut conn)?;
let presigned_url = state
@@ -614,8 +603,7 @@ pub async fn download_with_token(
}
let version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(claims.doc_id))
.filter(document_versions::version_number.eq(doc.current_version))
.find(doc.current_version_id)
.first(&mut conn)?;
let now = Utc::now().naive_utc();
@@ -697,8 +685,7 @@ pub async fn update_document(
}
let current_version: DocumentVersion = document_versions::table
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::version_number.eq(document.current_version))
.find(document.current_version_id)
.first(&mut conn)?;
let tags_map = load_tags_for_documents(&mut conn, &[document_id])?;
@@ -706,21 +693,7 @@ pub async fn update_document(
drop(conn);
let assets = load_asset_responses(&state, version_id).await?;
let mut grouped_assets = DocumentAssetGroupResponse::default();
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned()
{
grouped_assets.thumbnail = Some(asset);
}
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned()
{
grouped_assets.preview = Some(asset);
}
let version_response = to_version_response(current_version);
Ok(Json(DocumentDetailResponse {
document: to_document_response(
@@ -728,10 +701,8 @@ pub async fn update_document(
user.user_id,
document,
tags_map.get(&document_id).cloned(),
Some(grouped_assets),
Some((version_response, assets)),
)?,
current_version: to_version_response(current_version),
assets,
}))
}
@@ -982,9 +953,8 @@ async fn process_upload(
let existing = documents::table
.inner_join(
document_versions::table.on(document_versions::document_id
.eq(documents::id)
.and(document_versions::version_number.eq(documents::current_version))),
document_versions::table
.on(document_versions::id.eq(documents::current_version_id)),
)
.filter(document_versions::checksum.eq(&checksum_hex))
.select((documents::all_columns, document_versions::all_columns))
@@ -1008,21 +978,7 @@ async fn process_upload(
let tags = tags_map.get(&document.id).cloned();
drop(conn);
let assets = load_asset_responses(state, version.id).await?;
let mut grouped_assets = DocumentAssetGroupResponse::default();
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned()
{
grouped_assets.thumbnail = Some(asset);
}
if let Some(asset) = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned()
{
grouped_assets.preview = Some(asset);
}
let version_response = to_version_response(version.clone());
info!(
document_id = %document.id,
@@ -1037,10 +993,8 @@ async fn process_upload(
user_id,
document,
tags,
Some(grouped_assets),
Some((version_response, assets)),
)?,
current_version: to_version_response(version),
assets,
},
created: false,
});
@@ -1078,7 +1032,7 @@ async fn process_upload(
original_name: original_name.clone(),
content_type: content_type.clone(),
folder_id,
current_version: version_number,
current_version_id: version_id,
issued_at: None,
title: derive_document_title(&original_name),
metadata: metadata_value.clone(),
@@ -1109,9 +1063,13 @@ async fn process_upload(
};
let detail = DocumentDetailResponse {
document: to_document_response(state, user_id, document, None, None)?,
current_version: to_version_response(version.clone()),
assets: Vec::new(),
document: to_document_response(
state,
user_id,
document,
None,
Some((to_version_response(version.clone()), Vec::new())),
)?,
};
if let Ok(mut conn) = state.db() {
@@ -1171,111 +1129,61 @@ pub(crate) fn load_tags_for_documents(
pub(crate) async fn load_primary_assets(
state: &AppState,
documents: &[Document],
) -> AppResult<HashMap<Uuid, PrimaryDocumentAssets>> {
) -> AppResult<HashMap<Uuid, (DocumentVersionResponse, Vec<DocumentAssetResponse>)>> {
if documents.is_empty() {
return Ok(HashMap::new());
}
#[derive(Default)]
struct RawPrimaryAssets {
thumbnail: Option<DocumentAsset>,
preview: Option<DocumentAsset>,
let mut doc_to_version: HashMap<Uuid, Uuid> = HashMap::with_capacity(documents.len());
let mut version_ids: Vec<Uuid> = Vec::with_capacity(documents.len());
for doc in documents {
doc_to_version.insert(doc.id, doc.current_version_id);
version_ids.push(doc.current_version_id);
}
let mut current_versions: HashMap<Uuid, i32> = HashMap::new();
let mut doc_ids = Vec::with_capacity(documents.len());
for doc in documents {
current_versions.insert(doc.id, doc.current_version);
doc_ids.push(doc.id);
}
version_ids.sort();
version_ids.dedup();
let mut conn = state.db()?;
let versions: Vec<DocumentVersion> = document_versions::table
.filter(document_versions::document_id.eq_any(&doc_ids))
.filter(document_versions::id.eq_any(&version_ids))
.load(&mut conn)?;
let mut version_id_by_doc: HashMap<Uuid, Uuid> = HashMap::new();
let mut doc_id_by_version: HashMap<Uuid, Uuid> = HashMap::new();
let mut version_map: HashMap<Uuid, DocumentVersion> = HashMap::new();
for version in versions {
if let Some(current_number) = current_versions.get(&version.document_id) {
if *current_number == version.version_number {
version_id_by_doc.insert(version.document_id, version.id);
doc_id_by_version.insert(version.id, version.document_id);
}
}
version_map.insert(version.id, version);
}
if version_id_by_doc.is_empty() {
return Ok(HashMap::new());
}
let version_ids: Vec<Uuid> = version_id_by_doc.values().copied().collect();
let assets: Vec<DocumentAsset> = document_assets::table
.filter(document_assets::document_version_id.eq_any(&version_ids))
.filter(
document_assets::asset_type
.eq_any(vec!["thumbnail".to_string(), "preview".to_string()]),
)
.order((
document_assets::document_version_id.asc(),
document_assets::created_at.asc(),
))
.load(&mut conn)?;
let mut raw_assets: HashMap<Uuid, RawPrimaryAssets> = HashMap::new();
let mut assets_by_version: HashMap<Uuid, Vec<DocumentAssetResponse>> = HashMap::new();
for asset in assets {
if let Some(doc_id) = doc_id_by_version.get(&asset.document_version_id) {
let entry = raw_assets.entry(*doc_id).or_default();
let asset_type = asset.asset_type.clone();
match asset_type.as_str() {
"thumbnail" => {
if entry.thumbnail.is_none() {
entry.thumbnail = Some(asset);
}
}
"preview" => {
if entry.preview.is_none() {
entry.preview = Some(asset);
}
}
_ => {}
}
}
let version_id = asset.document_version_id;
let response = to_asset_response(asset, None);
assets_by_version
.entry(version_id)
.or_default()
.push(response);
}
drop(conn);
let mut responses = HashMap::with_capacity(raw_assets.len());
for (doc_id, raw) in raw_assets {
let mut assets = PrimaryDocumentAssets::default();
if let Some(asset) = raw.preview {
let s3_key = asset.s3_key.clone();
let url = state
.storage
.presign_get_object(&s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
assets.preview = Some(to_asset_response(asset, url));
}
if let Some(asset) = raw.thumbnail {
let s3_key = asset.s3_key.clone();
let url = state
.storage
.presign_get_object(&s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
assets.thumbnail = Some(to_asset_response(asset, url));
}
if assets.thumbnail.is_some() || assets.preview.is_some() {
responses.insert(doc_id, assets);
let mut result: HashMap<Uuid, (DocumentVersionResponse, Vec<DocumentAssetResponse>)> =
HashMap::with_capacity(doc_to_version.len());
for (doc_id, version_id) in doc_to_version {
if let Some(version) = version_map.remove(&version_id) {
let assets = assets_by_version.remove(&version_id).unwrap_or_default();
result.insert(doc_id, (to_version_response(version), assets));
}
}
Ok(responses)
Ok(result)
}
pub(crate) fn to_document_response(
@@ -1283,9 +1191,18 @@ pub(crate) fn to_document_response(
user_id: Uuid,
doc: Document,
tags: Option<Vec<Tag>>,
assets: Option<DocumentAssetGroupResponse>,
current_version: Option<(DocumentVersionResponse, Vec<DocumentAssetResponse>)>,
) -> AppResult<DocumentResponse> {
let download_path = build_download_path(state, doc.id, user_id)?;
let current_version = if let Some((version, assets)) = current_version {
let download_path = build_download_path(state, doc.id, user_id)?;
Some(DocumentCurrentVersionResponse {
version,
assets,
download_path,
})
} else {
None
};
Ok(DocumentResponse {
id: doc.id,
@@ -1294,7 +1211,6 @@ pub(crate) fn to_document_response(
original_name: doc.original_name,
content_type: doc.content_type,
folder_id: doc.folder_id,
current_version: doc.current_version,
uploaded_at: to_iso(doc.uploaded_at),
updated_at: to_iso(doc.updated_at),
deleted_at: doc.deleted_at.map(to_iso),
@@ -1305,8 +1221,7 @@ pub(crate) fn to_document_response(
.into_iter()
.map(TagResponse::from)
.collect(),
assets: assets.unwrap_or_default(),
download_path,
current_version,
})
}
@@ -1330,13 +1245,13 @@ fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse {
}
}
fn to_asset_response(asset: DocumentAsset, url: String) -> DocumentAssetResponse {
fn to_asset_response(asset: DocumentAsset, url: Option<String>) -> DocumentAssetResponse {
let metadata = asset.metadata.clone();
DocumentAssetResponse {
id: asset.id,
asset_type: asset.asset_type,
mime_type: asset.mime_type,
width: asset.width,
height: asset.height,
metadata,
url,
created_at: to_iso(asset.created_at),
}
@@ -1369,20 +1284,10 @@ async fn load_asset_responses(
.load(&mut conn)?;
drop(conn);
let mut responses = Vec::with_capacity(assets.len());
for asset in assets {
let url = state
.storage
.presign_get_object(
&asset.s3_key,
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
)
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
responses.push(to_asset_response(asset, url));
}
Ok(responses)
Ok(assets
.into_iter()
.map(|asset| to_asset_response(asset, None))
.collect())
}
pub(crate) fn to_iso(dt: NaiveDateTime) -> String {
+6 -6
View File
@@ -215,18 +215,18 @@ pub async fn list_folder_contents(
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
drop(conn);
let primary_assets = load_primary_assets(&state, &docs).await?;
let primary_versions = load_primary_assets(&state, &docs).await?;
let mut documents = Vec::with_capacity(doc_ids.len());
for doc in docs {
let tags = tags_map.get(&doc.id).cloned();
let assets = primary_assets.get(&doc.id).cloned();
let current_version = primary_versions.get(&doc.id).cloned();
documents.push(to_document_response(
&state,
user.user_id,
doc,
tags,
assets,
current_version,
)?);
}
@@ -399,17 +399,17 @@ pub async fn search_documents(
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
drop(conn);
let primary_assets = load_primary_assets(&state, &docs).await?;
let primary_versions = load_primary_assets(&state, &docs).await?;
let mut response = Vec::with_capacity(doc_ids.len());
for doc in docs {
let tags = tags_map.get(&doc.id).cloned();
let assets = primary_assets.get(&doc.id).cloned();
let current_version = primary_versions.get(&doc.id).cloned();
response.push(to_document_response(
&state,
user.user_id,
doc,
tags,
assets,
current_version,
)?);
}
+1
View File
@@ -69,6 +69,7 @@ pub fn create_router(state: AppState) -> Router<()> {
.patch(documents::update_document),
)
.route("/:id/download", get(documents::download_document))
.route("/:id/assets/:asset_id", get(documents::get_document_asset))
.route(
"/:id/assets",
get(documents::list_document_assets).post(documents::request_document_assets),
+1 -4
View File
@@ -7,8 +7,6 @@ diesel::table! {
asset_type -> Text,
s3_key -> Text,
mime_type -> Text,
width -> Nullable<Int4>,
height -> Nullable<Int4>,
metadata -> Jsonb,
created_at -> Timestamptz,
}
@@ -48,7 +46,6 @@ diesel::table! {
#[max_length = 100]
content_type -> Nullable<Varchar>,
folder_id -> Nullable<Uuid>,
current_version -> Int4,
uploaded_at -> Timestamptz,
updated_at -> Timestamptz,
deleted_at -> Nullable<Timestamptz>,
@@ -56,6 +53,7 @@ diesel::table! {
issued_at -> Nullable<Timestamptz>,
#[max_length = 255]
title -> Varchar,
current_version_id -> Uuid,
}
}
@@ -128,7 +126,6 @@ diesel::joinable!(document_assets -> document_versions (document_version_id));
diesel::joinable!(document_tags -> documents (document_id));
diesel::joinable!(document_tags -> tags (tag_id));
diesel::joinable!(document_tags -> users (assigned_by));
diesel::joinable!(document_versions -> documents (document_id));
diesel::joinable!(documents -> folders (folder_id));
diesel::joinable!(refresh_tokens -> users (user_id));
-2
View File
@@ -377,8 +377,6 @@ fn persist_ocr_metadata(
asset_type: OCR_TEXT_ASSET_TYPE.to_string(),
s3_key: s3_key.to_string(),
mime_type: "text/plain".to_string(),
width: None,
height: None,
metadata: json!({
"generated_at": Utc::now().to_rfc3339(),
"source": source,
+2 -4
View File
@@ -405,10 +405,10 @@ fn persist_assets_metadata(
asset_type: asset.asset_type.to_string(),
s3_key: asset.s3_key.to_string(),
mime_type: "image/png".to_string(),
width: asset.generated.width,
height: asset.generated.height,
metadata: json!({
"generated_at": Utc::now().to_rfc3339(),
"width": asset.generated.width,
"height": asset.generated.height,
}),
};
@@ -422,8 +422,6 @@ fn persist_assets_metadata(
.set((
document_assets::s3_key.eq(excluded(document_assets::s3_key)),
document_assets::mime_type.eq(excluded(document_assets::mime_type)),
document_assets::width.eq(excluded(document_assets::width)),
document_assets::height.eq(excluded(document_assets::height)),
document_assets::metadata.eq(excluded(document_assets::metadata)),
))
.execute(&mut conn)