From 44680114299059ddd3c8c35f0950ec88445310b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 27 Jul 2026 16:25:49 +0200 Subject: [PATCH] update rustpad --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/auth/mod.rs | 33 +++++++- src/db.rs | 192 ++++++++++------------------------------------ src/main.rs | 1 + src/queries.rs | 18 +++-- src/row_decode.rs | 30 ++++++++ 7 files changed, 114 insertions(+), 164 deletions(-) create mode 100644 src/row_decode.rs diff --git a/Cargo.lock b/Cargo.lock index cf1b5db..0df1cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.1.6" +version = "0.1.7" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 444f2ee..d3c3f20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.1.6" +version = "0.1.7" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/src/auth/mod.rs b/src/auth/mod.rs index d0b6204..e82c350 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -20,7 +20,7 @@ use lettre::{ use rand_core::{OsRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use sqlx::FromRow; +use sqlx::{any::AnyRow, FromRow, Row}; use tracing::{debug, info, warn}; use crate::{ @@ -32,7 +32,7 @@ const MIN_PASSWORD: usize = 8; const MAX_PASSWORD: usize = 128; const MAX_NICKNAME: usize = 40; -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] pub struct User { pub id: i64, pub nickname: String, @@ -103,7 +103,7 @@ pub struct ResourceActionRequest { #[serde(default)] password: Option, } -#[derive(Serialize, FromRow)] +#[derive(Serialize)] pub struct ResourceItem { slug: String, title: String, @@ -117,6 +117,33 @@ pub struct ResourceItem { shared_by: String, } +impl<'r> sqlx::FromRow<'r, AnyRow> for User { + fn from_row(row: &'r AnyRow) -> Result { + Ok(Self { + id: row.try_get("id")?, + nickname: crate::row_decode::text(row, "nickname")?, + email: crate::row_decode::text(row, "email")?, + password_hash: crate::row_decode::text(row, "password_hash")?, + confirmed_at: crate::row_decode::optional_text(row, "confirmed_at")?, + }) + } +} + +impl<'r> sqlx::FromRow<'r, AnyRow> for ResourceItem { + fn from_row(row: &'r AnyRow) -> Result { + Ok(Self { + slug: crate::row_decode::text(row, "slug")?, + title: crate::row_decode::text(row, "title")?, + protected: row.try_get("protected")?, + updated_at: crate::row_decode::text(row, "updated_at")?, + private_resource: row.try_get("private")?, + owned: row.try_get("owned")?, + permission: crate::row_decode::text(row, "permission")?, + shared_by: crate::row_decode::text(row, "shared_by")?, + }) + } +} + #[derive(Deserialize)] pub struct PrivacyRequest { kind: String, diff --git a/src/db.rs b/src/db.rs index 8516434..917f470 100644 --- a/src/db.rs +++ b/src/db.rs @@ -7,32 +7,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use rand_core::{OsRng, RngCore}; use serde::Serialize; use sqlx::FromRow; -use sqlx::{Any, Row, Transaction, any::AnyRow}; - -fn any_text(row: &AnyRow, column: &str) -> Result { - match row.try_get::(column) { - Ok(value) => Ok(value), - Err(string_error) => match row.try_get::, _>(column) { - Ok(value) => { - String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error))) - } - Err(_) => Err(string_error), - }, - } -} - -fn any_optional_text(row: &AnyRow, column: &str) -> Result, sqlx::Error> { - match row.try_get::, _>(column) { - Ok(value) => Ok(value), - Err(string_error) => match row.try_get::>, _>(column) { - Ok(Some(value)) => String::from_utf8(value) - .map(Some) - .map_err(|error| sqlx::Error::Decode(Box::new(error))), - Ok(None) => Ok(None), - Err(_) => Err(string_error), - }, - } -} +use sqlx::{any::AnyRow, Any, Row, Transaction}; async fn inserted_id( kind: DatabaseKind, @@ -51,7 +26,7 @@ async fn inserted_id( sqlx::query_scalar(query).fetch_one(&mut **tx).await } -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] pub struct Workspace { pub id: i64, pub slug: String, @@ -62,19 +37,7 @@ pub struct Workspace { pub is_private: i64, } -fn workspace_from_any_row(row: AnyRow) -> Result { - Ok(Workspace { - id: row.try_get("id")?, - slug: any_text(&row, "slug")?, - title: any_text(&row, "title")?, - password_hash: any_optional_text(&row, "password_hash")?, - created_at: any_text(&row, "created_at")?, - updated_at: any_text(&row, "updated_at")?, - is_private: row.try_get("is_private")?, - }) -} - -#[derive(Debug, Clone, Serialize, FromRow)] +#[derive(Debug, Clone, Serialize)] pub struct Note { pub id: i64, #[serde(skip_serializing)] @@ -122,22 +85,7 @@ impl From for Note { } } -fn note_from_any_row(row: AnyRow) -> Result { - Ok(Note { - id: row.try_get("id")?, - _workspace_id: row.try_get("workspace_id")?, - slug: any_text(&row, "slug")?, - title: any_text(&row, "title")?, - content: any_text(&row, "content")?, - created_at: any_text(&row, "created_at")?, - updated_at: any_text(&row, "updated_at")?, - owner_map: any_text(&row, "owner_map")?, - protected: row.try_get("protected")?, - created_by: any_optional_text(&row, "created_by")?, - }) -} - -#[derive(Debug, Serialize, FromRow)] +#[derive(Debug, Serialize)] pub struct Revision { pub id: i64, pub content: String, @@ -146,25 +94,7 @@ pub struct Revision { pub owner_map: String, } -fn revision_from_any_row(row: AnyRow) -> Result { - Ok(Revision { - id: row.try_get("id")?, - content: any_text(&row, "content")?, - created_at: any_text(&row, "created_at")?, - author: any_optional_text(&row, "author")?, - owner_map: any_text(&row, "owner_map")?, - }) -} - pub async fn find_workspace(pool: &Database, slug: &str) -> Result, sqlx::Error> { - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q001)) - .bind(slug) - .fetch_optional(pool.pool()) - .await? - .map(workspace_from_any_row) - .transpose(); - } sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_optional(pool.pool()) @@ -187,14 +117,6 @@ pub async fn create_workspace( .execute(pool.pool()) .await?; - if pool.kind() == DatabaseKind::MySql { - return workspace_from_any_row( - sqlx::query(queries::get(pool.kind(), queries::Q001)) - .bind(slug) - .fetch_one(pool.pool()) - .await?, - ); - } sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_one(pool.pool()) @@ -229,15 +151,6 @@ pub async fn list_notes(pool: &Database, workspace_id: i64) -> Result, .map(Note::from) .collect()); } - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q003)) - .bind(workspace_id) - .fetch_all(pool.pool()) - .await? - .into_iter() - .map(note_from_any_row) - .collect(); - } sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q003)) .bind(workspace_id) .fetch_all(pool.pool()) @@ -257,15 +170,6 @@ pub async fn find_note( .await? .map(Note::from)); } - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q004)) - .bind(workspace_id) - .bind(slug) - .fetch_optional(pool.pool()) - .await? - .map(note_from_any_row) - .transpose(); - } sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004)) .bind(workspace_id) .bind(slug) @@ -331,15 +235,6 @@ pub async fn save_revision( } pub async fn list_revisions(pool: &Database, note_id: i64) -> Result, sqlx::Error> { - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q010)) - .bind(note_id) - .fetch_all(pool.pool()) - .await? - .into_iter() - .map(revision_from_any_row) - .collect(); - } sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q010)) .bind(note_id) .fetch_all(pool.pool()) @@ -406,7 +301,7 @@ pub fn normalize_timestamp(value: &str) -> String { value.to_string() } -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] pub struct Pad { pub id: i64, pub slug: String, @@ -419,29 +314,7 @@ pub struct Pad { pub is_private: i64, } -fn pad_from_any_row(row: AnyRow) -> Result { - Ok(Pad { - id: row.try_get("id")?, - slug: any_text(&row, "slug")?, - title: any_text(&row, "title")?, - content: any_text(&row, "content")?, - password_hash: any_optional_text(&row, "password_hash")?, - created_at: any_text(&row, "created_at")?, - updated_at: any_text(&row, "updated_at")?, - owner_map: any_text(&row, "owner_map")?, - is_private: row.try_get("is_private")?, - }) -} - pub async fn find_pad(pool: &Database, slug: &str) -> Result, sqlx::Error> { - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q011)) - .bind(slug) - .fetch_optional(pool.pool()) - .await? - .map(pad_from_any_row) - .transpose(); - } sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_optional(pool.pool()) @@ -464,14 +337,6 @@ pub async fn create_pad( .execute(pool.pool()) .await?; - if pool.kind() == DatabaseKind::MySql { - return pad_from_any_row( - sqlx::query(queries::get(pool.kind(), queries::Q011)) - .bind(slug) - .fetch_one(pool.pool()) - .await?, - ); - } sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_one(pool.pool()) @@ -530,15 +395,6 @@ pub async fn list_pad_revisions( pool: &Database, pad_id: i64, ) -> Result, sqlx::Error> { - if pool.kind() == DatabaseKind::MySql { - return sqlx::query(queries::get(pool.kind(), queries::Q016)) - .bind(pad_id) - .fetch_all(pool.pool()) - .await? - .into_iter() - .map(revision_from_any_row) - .collect(); - } sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q016)) .bind(pad_id) .fetch_all(pool.pool()) @@ -556,7 +412,7 @@ pub struct PublishedPage { pub updated_at: String, } -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] struct PublishedPageRow { token: String, pad_id: Option, @@ -567,7 +423,7 @@ struct PublishedPageRow { updated_at: String, } -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone)] struct PostgresPublishedPageRow { token: String, pad_id: Option, @@ -863,7 +719,7 @@ pub async fn find_file_owner( Ok(None) } -#[derive(Debug, Clone, Serialize, FromRow)] +#[derive(Debug, Clone, Serialize)] pub struct NoteFile { pub id: i64, pub filename: String, @@ -1079,3 +935,35 @@ pub async fn delete_pad_file( .await?; Ok(()) } + + +impl<'r> sqlx::FromRow<'r, AnyRow> for Workspace { + fn from_row(row: &'r AnyRow) -> Result { + Ok(Self { id: row.try_get("id")?, slug: crate::row_decode::text(row, "slug")?, title: crate::row_decode::text(row, "title")?, password_hash: crate::row_decode::optional_text(row, "password_hash")?, created_at: crate::row_decode::text(row, "created_at")?, updated_at: crate::row_decode::text(row, "updated_at")?, is_private: row.try_get("is_private")? }) + } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for Note { + fn from_row(row: &'r AnyRow) -> Result { + let protected: i64 = row.try_get("protected")?; + Ok(Self { id: row.try_get("id")?, _workspace_id: row.try_get("workspace_id")?, slug: crate::row_decode::text(row, "slug")?, title: crate::row_decode::text(row, "title")?, content: crate::row_decode::text(row, "content")?, created_at: crate::row_decode::text(row, "created_at")?, updated_at: crate::row_decode::text(row, "updated_at")?, owner_map: crate::row_decode::text(row, "owner_map")?, protected: protected != 0, created_by: crate::row_decode::optional_text(row, "created_by")? }) + } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for Revision { + fn from_row(row: &'r AnyRow) -> Result { + Ok(Self { id: row.try_get("id")?, content: crate::row_decode::text(row, "content")?, created_at: crate::row_decode::text(row, "created_at")?, author: crate::row_decode::optional_text(row, "author")?, owner_map: crate::row_decode::text(row, "owner_map")? }) + } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for Pad { + fn from_row(row: &'r AnyRow) -> Result { + Ok(Self { id: row.try_get("id")?, slug: crate::row_decode::text(row, "slug")?, title: crate::row_decode::text(row, "title")?, content: crate::row_decode::text(row, "content")?, password_hash: crate::row_decode::optional_text(row, "password_hash")?, created_at: crate::row_decode::text(row, "created_at")?, updated_at: crate::row_decode::text(row, "updated_at")?, owner_map: crate::row_decode::text(row, "owner_map")?, is_private: row.try_get("is_private")? }) + } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for PublishedPageRow { + fn from_row(row: &'r AnyRow) -> Result { Ok(Self { token: crate::row_decode::text(row,"token")?, pad_id: row.try_get("pad_id")?, note_id: row.try_get("note_id")?, allow_task_updates: row.try_get("allow_task_updates")?, title: crate::row_decode::text(row,"title")?, content: crate::row_decode::text(row,"content")?, updated_at: crate::row_decode::text(row,"updated_at")? }) } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow { + fn from_row(row: &'r AnyRow) -> Result { Ok(Self { token: crate::row_decode::text(row,"token")?, pad_id: row.try_get("pad_id")?, note_id: row.try_get("note_id")?, allow_task_updates: row.try_get("allow_task_updates")?, title: crate::row_decode::text(row,"title")?, content: crate::row_decode::text(row,"content")?, updated_at: crate::row_decode::text(row,"updated_at")? }) } +} +impl<'r> sqlx::FromRow<'r, AnyRow> for NoteFile { + fn from_row(row: &'r AnyRow) -> Result { let attached:i64=row.try_get("is_attached")?; Ok(Self { id:row.try_get("id")?, filename:crate::row_decode::text(row,"filename")?, url:crate::row_decode::text(row,"url")?, mime_type:crate::row_decode::text(row,"mime_type")?, size_bytes:row.try_get("size_bytes")?, created_at:crate::row_decode::text(row,"created_at")?, is_attached:attached!=0, detached_at:crate::row_decode::optional_text(row,"detached_at")? }) } +} diff --git a/src/main.rs b/src/main.rs index 26c9ed7..04a6c3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod row_decode; mod api; mod app; mod assets; diff --git a/src/queries.rs b/src/queries.rs index b37fb46..865afaf 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -129,9 +129,9 @@ pub const Q001_MYSQL: &str = "SELECT id, slug, CAST(title AS CHAR CHARACTER SET pub const Q001_POSTGRES: &str = "SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM workspaces WHERE slug = ?"; pub const Q002: &str = "INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)"; pub const Q003: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"; -pub const Q003_MYSQL: &str = "SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"; +pub const Q003_MYSQL: &str = "SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"; pub const Q004: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? AND slug = ?"; -pub const Q004_MYSQL: &str = "SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? AND slug = ?"; +pub const Q004_MYSQL: &str = "SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? AND slug = ?"; pub const Q005: &str = "INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"; pub const Q006: &str = @@ -159,7 +159,7 @@ pub const Q018: &str = "INSERT INTO published_pages (token, pad_id) VALUES (?, ? pub const Q019: &str = "SELECT token FROM published_pages WHERE note_id = ?"; pub const Q020: &str = "INSERT INTO published_pages (token, note_id) VALUES (?, ?)"; pub const Q021: &str = "SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"; -pub const Q021_MYSQL: &str = "SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, CAST(COALESCE(p.title, n.title) AS CHAR CHARACTER SET utf8mb4) AS title, CAST(COALESCE(p.content, n.content) AS CHAR CHARACTER SET utf8mb4) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"; +pub const Q021_MYSQL: &str = "SELECT pp.token, pp.pad_id, pp.note_id, CAST(CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS SIGNED) AS allow_task_updates, CAST(COALESCE(p.title, n.title) AS CHAR CHARACTER SET utf8mb4) AS title, CAST(COALESCE(p.content, n.content) AS CHAR CHARACTER SET utf8mb4) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"; pub const Q022: &str = "SELECT file_token FROM pads WHERE id = ?"; pub const Q023: &str = "UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL"; pub const Q024: &str = "SELECT file_token FROM notes WHERE id = ?"; @@ -176,18 +176,18 @@ pub const Q031: &str = "DELETE FROM notes WHERE id = ?"; pub const Q032: &str = "INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"; pub const Q033: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"; -pub const Q033_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"; +pub const Q033_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"; pub const Q034: &str = "UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?"; pub const Q035: &str = "INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"; pub const Q036: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"; -pub const Q036_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"; +pub const Q036_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"; pub const Q037: &str = "UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?"; pub const Q038: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"; -pub const Q038_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"; +pub const Q038_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"; pub const Q039: &str = "DELETE FROM note_files WHERE id = ? AND note_id = ?"; pub const Q046: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"; -pub const Q046_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"; +pub const Q046_MYSQL: &str = "SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"; pub const Q047: &str = "DELETE FROM pad_files WHERE id = ? AND pad_id = ?"; static POSTGRES_QUERIES: OnceLock>> = OnceLock::new(); @@ -220,6 +220,8 @@ pub fn get(kind: DatabaseKind, query: &'static str) -> &'static str { (DatabaseKind::MySql, Q036) => Q036_MYSQL, (DatabaseKind::MySql, Q038) => Q038_MYSQL, (DatabaseKind::MySql, Q046) => Q046_MYSQL, + (DatabaseKind::MySql, Q044) => Q044_MYSQL, + (DatabaseKind::MySql, Q045) => Q045_MYSQL, _ => query, }; @@ -258,6 +260,8 @@ pub const Q044: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?"; pub const Q045: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?"; +pub const Q044_MYSQL: &str = "SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE pad_id = ?"; +pub const Q045_MYSQL: &str = "SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE note_id = ?"; pub const Q021_POSTGRES: &str = "SELECT pp.token, pp.pad_id, pp.note_id, pp.allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = $1"; pub const Q044_POSTGRES: &str = "SELECT allow_task_updates FROM published_pages WHERE pad_id = $1"; diff --git a/src/row_decode.rs b/src/row_decode.rs new file mode 100644 index 0000000..045be14 --- /dev/null +++ b/src/row_decode.rs @@ -0,0 +1,30 @@ +use sqlx::{any::AnyRow, ColumnIndex, Error, Row}; + +pub fn text(row: &AnyRow, index: I) -> Result +where + I: ColumnIndex + Copy, +{ + match row.try_get::(index) { + Ok(value) => Ok(value), + Err(string_error) => match row.try_get::, _>(index) { + Ok(value) => String::from_utf8(value).map_err(|error| Error::Decode(Box::new(error))), + Err(_) => Err(string_error), + }, + } +} + +pub fn optional_text(row: &AnyRow, index: I) -> Result, Error> +where + I: ColumnIndex + Copy, +{ + match row.try_get::, _>(index) { + Ok(value) => Ok(value), + Err(string_error) => match row.try_get::>, _>(index) { + Ok(Some(value)) => String::from_utf8(value) + .map(Some) + .map_err(|error| Error::Decode(Box::new(error))), + Ok(None) => Ok(None), + Err(_) => Err(string_error), + }, + } +}