From 36e0543bcd7bb010e1a303c55961129f7006fb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 27 Jul 2026 16:11:03 +0200 Subject: [PATCH] fix in mysql --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/db.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++++++- src/queries.rs | 52 +++++++++--------- 4 files changed, 172 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e029b29..cf1b5db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.1.5" +version = "0.1.6" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 1fe5ce0..444f2ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/src/db.rs b/src/db.rs index 3be13ab..252dda9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -7,7 +7,31 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use rand_core::{OsRng, RngCore}; use serde::Serialize; use sqlx::FromRow; -use sqlx::{Any, Transaction}; +use sqlx::{Any, AnyRow, Row, Transaction}; + + +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), + }, + } +} async fn inserted_id( kind: DatabaseKind, @@ -37,6 +61,18 @@ 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)] pub struct Note { pub id: i64, @@ -85,6 +121,21 @@ 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)] pub struct Revision { pub id: i64, @@ -94,7 +145,25 @@ 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()) @@ -117,6 +186,14 @@ 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()) @@ -151,6 +228,15 @@ 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()) @@ -170,6 +256,15 @@ 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) @@ -235,6 +330,15 @@ 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()) @@ -314,7 +418,29 @@ 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()) @@ -337,6 +463,14 @@ 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()) @@ -395,6 +529,15 @@ 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()) diff --git a/src/queries.rs b/src/queries.rs index 8fda435..b37fb46 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -26,7 +26,7 @@ pub const RESOURCE_COLOR_BY_USER: &str = "SELECT color FROM user_resource_colors pub const RESOURCE_COLOR_DELETE: &str = "DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"; pub const RESOURCE_COLOR_INSERT: &str = "INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"; pub const AUTH_ACCOUNT_ACTION_BY_TOKEN: &str = "SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"; -pub const AUTH_ACCOUNT_ACTION_BY_TOKEN_MYSQL: &str = "SELECT user_id, action, CONVERT(payload USING utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"; +pub const AUTH_ACCOUNT_ACTION_BY_TOKEN_MYSQL: &str = "SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"; pub const AUTH_CONSUME_ACCOUNT_ACTION: &str = "UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"; pub const AUTH_UPDATE_EMAIL: &str = @@ -42,7 +42,7 @@ pub const AUTH_UPDATE_DIRECTORY_USER: &str = "UPDATE users SET nickname = ?, nic pub const AUTH_DIRECTORY_PROFILE_BY_USER: &str = "SELECT auth_provider, directory_display_name FROM users WHERE id = ?"; pub const AUTH_USER_BY_EXTERNAL_ID: &str = "SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"; -pub const AUTH_USER_BY_EXTERNAL_ID_MYSQL: &str = "SELECT id, nickname, email, CONVERT(password_hash USING utf8mb4) AS password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"; +pub const AUTH_USER_BY_EXTERNAL_ID_MYSQL: &str = "SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"; pub const AUTH_DELETE_USER: &str = "DELETE FROM users WHERE id = ?"; pub const AUTH_SESSION_EXPIRES_AT: &str = "SELECT expires_at FROM user_sessions WHERE token = ?"; pub const AUTH_DELETE_SESSION_BY_TOKEN: &str = "DELETE FROM user_sessions WHERE token = ?"; @@ -68,24 +68,24 @@ pub const AUTH_UPDATE_PASSWORD: &str = pub const AUTH_CONSUME_RESET_TOKEN: &str = "UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"; pub const AUTH_DELETE_SESSIONS_BY_USER: &str = "DELETE FROM user_sessions WHERE user_id = ?"; pub const AUTH_USER_BY_SESSION: &str = "SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ?"; -pub const AUTH_USER_BY_SESSION_MYSQL: &str = "SELECT u.id, u.nickname, u.email, CONVERT(u.password_hash USING utf8mb4) AS password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ?"; +pub const AUTH_USER_BY_SESSION_MYSQL: &str = "SELECT u.id, u.nickname, u.email, CAST(u.password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ?"; pub const AUTH_INSERT_SESSION: &str = "INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"; pub const AUTH_USER_BY_NICKNAME: &str = "SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE nickname_key = ?"; pub const AUTH_USER_BY_NICKNAME_MYSQL: &str = - "SELECT id, nickname, email, CONVERT(password_hash USING utf8mb4) AS password_hash, confirmed_at FROM users WHERE nickname_key = ?"; + "SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE nickname_key = ?"; pub const AUTH_USER_BY_EMAIL: &str = "SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE email_key = ?"; pub const AUTH_USER_BY_EMAIL_MYSQL: &str = - "SELECT id, nickname, email, CONVERT(password_hash USING utf8mb4) AS password_hash, confirmed_at FROM users WHERE email_key = ?"; + "SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE email_key = ?"; pub const USER_ATTACH_WORKSPACE: &str = "INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"; pub const USER_ATTACH_PAD: &str = "INSERT INTO user_pads (user_id, pad_id) SELECT ?, id FROM pads WHERE slug = ?"; pub const USER_LIST_WORKSPACES: &str = "SELECT w.slug, w.title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS protected, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? UNION SELECT w.slug, w.title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = ? ORDER BY updated_at DESC"; -pub const USER_LIST_WORKSPACES_MYSQL: &str = "SELECT w.slug, CONVERT(w.title USING utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS protected, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? UNION SELECT w.slug, CONVERT(w.title USING utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = ? ORDER BY updated_at DESC"; +pub const USER_LIST_WORKSPACES_MYSQL: &str = "SELECT w.slug, CAST(w.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS protected, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? UNION SELECT w.slug, CAST(w.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = ? ORDER BY updated_at DESC"; pub const USER_LIST_PADS: &str = "SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"; -pub const USER_LIST_PADS_MYSQL: &str = "SELECT p.slug, CONVERT(p.title USING utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, CONVERT(p.title USING utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"; +pub const USER_LIST_PADS_MYSQL: &str = "SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"; pub const USER_OWNS_WORKSPACE: &str = "SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"; pub const USER_OWNS_PAD: &str = "SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"; pub const USER_DELETE_WORKSPACE: &str = "DELETE FROM workspaces WHERE slug = ?"; @@ -109,14 +109,14 @@ pub const SHARE_INVITATION_INSERT: &str = "INSERT INTO resource_share_invitation pub const SHARE_INVITATION_DELETE_TOKEN: &str = "DELETE FROM resource_share_invitations WHERE token_hash = ?"; pub const SHARE_INVITATION_FIND_TOKEN: &str = "SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = ?"; -pub const SHARE_INVITATION_FIND_TOKEN_MYSQL: &str = "SELECT resource_kind, resource_slug, user_id, permission, CONVERT(expires_at USING utf8mb4) AS expires_at, CONVERT(accepted_at USING utf8mb4) AS accepted_at FROM resource_share_invitations WHERE token_hash = ?"; +pub const SHARE_INVITATION_FIND_TOKEN_MYSQL: &str = "SELECT resource_kind, resource_slug, user_id, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(accepted_at AS CHAR CHARACTER SET utf8mb4) AS accepted_at FROM resource_share_invitations WHERE token_hash = ?"; pub const SHARE_INVITATION_ACCEPT: &str = "UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"; pub const RESOURCE_SHARING_USERS: &str = "SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"; pub const RESOURCE_SHARING_LINKS: &str = "SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"; -pub const RESOURCE_SHARING_LINKS_MYSQL: &str = "SELECT token_hash, CONVERT(token USING utf8mb4) AS token, permission, CONVERT(expires_at USING utf8mb4) AS expires_at, CONVERT(created_at USING utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"; +pub const RESOURCE_SHARING_LINKS_MYSQL: &str = "SELECT token_hash, CAST(token AS CHAR CHARACTER SET utf8mb4) AS token, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(created_at AS CHAR CHARACTER SET utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"; pub const RESOURCE_SHARING_PENDING: &str = "SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"; -pub const RESOURCE_SHARING_PENDING_MYSQL: &str = "SELECT u.email, u.nickname, i.permission, CONVERT(i.expires_at USING utf8mb4) AS expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"; +pub const RESOURCE_SHARING_PENDING_MYSQL: &str = "SELECT u.email, u.nickname, i.permission, CAST(i.expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"; pub const SHARE_LINK_INSERT: &str = "INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"; pub const SHARE_LINK_UPDATE: &str = "UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"; pub const SHARE_LINK_REVOKE: &str = "UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"; @@ -125,13 +125,13 @@ pub const SHARE_LINK_PERMISSION: &str = "SELECT permission FROM resource_share_l // SQLite uses INTEGER as the portable 64-bit integer cast target. pub const Q001: &str = "SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM workspaces WHERE slug = ?"; -pub const Q001_MYSQL: &str = "SELECT id, slug, CONVERT(title USING utf8mb4) AS title, CONVERT(password_hash USING utf8mb4) AS password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM workspaces WHERE slug = ?"; +pub const Q001_MYSQL: &str = "SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM workspaces WHERE slug = ?"; 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, CONVERT(title USING utf8mb4) AS title, CONVERT(content USING utf8mb4) AS content, created_at, updated_at, CONVERT(owner_map USING utf8mb4) AS owner_map, protected, CONVERT(created_by USING 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, 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, CONVERT(title USING utf8mb4) AS title, CONVERT(content USING utf8mb4) AS content, created_at, updated_at, CONVERT(owner_map USING utf8mb4) AS owner_map, protected, CONVERT(created_by USING 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, 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 = @@ -141,10 +141,10 @@ pub const Q008: &str = "INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"; pub const Q009: &str = "SELECT updated_at FROM notes WHERE id = ?"; pub const Q010: &str = "SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"; -pub const Q010_MYSQL: &str = "SELECT id, CONVERT(content USING utf8mb4) AS content, created_at, CONVERT(author USING utf8mb4) AS author, CONVERT(owner_map USING utf8mb4) AS owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"; +pub const Q010_MYSQL: &str = "SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"; // Keep privacy flags normalized to i64 across all SQLx Any drivers. pub const Q011: &str = "SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM pads WHERE slug = ?"; -pub const Q011_MYSQL: &str = "SELECT id, slug, CONVERT(title USING utf8mb4) AS title, CONVERT(content USING utf8mb4) AS content, CONVERT(password_hash USING utf8mb4) AS password_hash, created_at, updated_at, CONVERT(owner_map USING utf8mb4) AS owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM pads WHERE slug = ?"; +pub const Q011_MYSQL: &str = "SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM pads WHERE slug = ?"; pub const Q011_POSTGRES: &str = "SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM pads WHERE slug = ?"; pub const Q012: &str = "INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)"; pub const Q013: &str = @@ -153,13 +153,13 @@ pub const Q014: &str = "INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"; pub const Q015: &str = "SELECT updated_at FROM pads WHERE id = ?"; pub const Q016: &str = "SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"; -pub const Q016_MYSQL: &str = "SELECT id, CONVERT(content USING utf8mb4) AS content, created_at, CONVERT(author USING utf8mb4) AS author, CONVERT(owner_map USING utf8mb4) AS owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"; +pub const Q016_MYSQL: &str = "SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"; pub const Q017: &str = "SELECT token FROM published_pages WHERE pad_id = ?"; 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, CONVERT(COALESCE(p.title, n.title) USING utf8mb4) AS title, CONVERT(COALESCE(p.content, n.content) USING 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, 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 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 = ?"; @@ -167,27 +167,27 @@ pub const Q025: &str = "UPDATE notes SET file_token = ? WHERE id = ? AND file_to pub const Q026: &str = "SELECT id FROM pads WHERE file_token = ?"; pub const Q027: &str = "SELECT id FROM notes WHERE file_token = ?"; pub const Q028: &str = "SELECT content FROM note_revisions WHERE id = ? AND note_id = ?"; -pub const Q028_MYSQL: &str = "SELECT CONVERT(content USING utf8mb4) FROM note_revisions WHERE id = ? AND note_id = ?"; +pub const Q028_MYSQL: &str = "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM note_revisions WHERE id = ? AND note_id = ?"; pub const Q029: &str = "SELECT content FROM revisions WHERE id = ? AND pad_id = ?"; -pub const Q029_MYSQL: &str = "SELECT CONVERT(content USING utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"; +pub const Q029_MYSQL: &str = "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"; pub const Q030: &str = "SELECT owner_map FROM revisions WHERE id = ? AND pad_id = ?"; -pub const Q030_MYSQL: &str = "SELECT CONVERT(owner_map USING utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"; +pub const Q030_MYSQL: &str = "SELECT CAST(owner_map AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"; 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, CONVERT(filename USING 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, 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, CONVERT(filename USING 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, 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, CONVERT(filename USING 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, 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, CONVERT(filename USING 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, 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(); @@ -288,7 +288,7 @@ mod tests { } #[test] - fn mysql_text_columns_are_converted_to_character_data() { + fn mysql_text_columns_are_cast_to_character_data() { for query in [ Q001, Q003, Q004, Q010, Q011, Q016, Q021, Q028, Q029, Q030, Q033, Q036, Q038, Q046, AUTH_ACCOUNT_ACTION_BY_TOKEN, AUTH_USER_BY_EXTERNAL_ID, @@ -297,7 +297,7 @@ mod tests { RESOURCE_SHARING_LINKS, RESOURCE_SHARING_PENDING, ] { assert!( - get(DatabaseKind::MySql, query).contains("USING utf8mb4"), + get(DatabaseKind::MySql, query).contains("CHAR CHARACTER SET utf8mb4"), "missing MySQL text cast for query: {query}" ); }