306 lines
29 KiB
Rust
306 lines
29 KiB
Rust
use crate::database::DatabaseKind;
|
|
use std::{
|
|
collections::HashMap,
|
|
sync::{Mutex, OnceLock},
|
|
};
|
|
|
|
// Database bootstrap and identity helpers.
|
|
pub const SQLITE_FOREIGN_KEYS_ON: &str = "PRAGMA foreign_keys = ON";
|
|
pub const SQLITE_JOURNAL_WAL: &str = "PRAGMA journal_mode = WAL";
|
|
pub const SQLITE_BUSY_TIMEOUT: &str = "PRAGMA busy_timeout = 5000";
|
|
pub const SQLITE_LAST_INSERT_ID: &str = "SELECT last_insert_rowid()";
|
|
pub const MYSQL_LAST_INSERT_ID: &str = "SELECT LAST_INSERT_ID()";
|
|
pub const POSTGRES_NOTE_REVISION_LAST_INSERT_ID: &str =
|
|
"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))";
|
|
pub const POSTGRES_PAD_REVISION_LAST_INSERT_ID: &str =
|
|
"SELECT currval(pg_get_serial_sequence('revisions', 'id'))";
|
|
|
|
// Authentication queries.
|
|
pub const AUTH_LATEST_CONFIRMATION_CREATED_AT: &str = "SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1";
|
|
pub const AUTH_UPDATE_NICKNAME: &str =
|
|
"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?";
|
|
pub const AUTH_UPDATE_EDITOR_COLOR: &str =
|
|
"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?";
|
|
pub const AUTH_EDITOR_COLOR_BY_USER: &str = "SELECT editor_color FROM users WHERE id = ?";
|
|
pub const RESOURCE_COLOR_BY_USER: &str = "SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?";
|
|
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, 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 =
|
|
"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?";
|
|
pub const AUTH_DELETE_ACCOUNT_ACTIONS: &str =
|
|
"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?";
|
|
pub const AUTH_INSERT_ACCOUNT_ACTION: &str = "INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)";
|
|
pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: &str =
|
|
"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?";
|
|
pub const AUTH_INSERT_USER: &str = "INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES (?, ?, ?, ?, ?, ?)";
|
|
pub const AUTH_INSERT_DIRECTORY_USER: &str = "INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
pub const AUTH_UPDATE_DIRECTORY_USER: &str = "UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?";
|
|
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, 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 = ?";
|
|
pub const AUTH_REFRESH_SESSION: &str =
|
|
"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?";
|
|
pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: &str =
|
|
"DELETE FROM account_confirmation_tokens WHERE user_id = ?";
|
|
pub const AUTH_INSERT_CONFIRMATION_TOKEN: &str =
|
|
"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)";
|
|
pub const AUTH_FIND_CONFIRMATION_TOKEN: &str =
|
|
"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?";
|
|
pub const AUTH_CONFIRM_USER: &str =
|
|
"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?";
|
|
pub const AUTH_CONSUME_CONFIRMATION_TOKEN: &str = "UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?";
|
|
pub const AUTH_DELETE_RESET_TOKENS_BY_USER: &str =
|
|
"DELETE FROM password_reset_tokens WHERE user_id = ?";
|
|
pub const AUTH_INSERT_RESET_TOKEN: &str =
|
|
"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)";
|
|
pub const AUTH_FIND_RESET_TOKEN: &str =
|
|
"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?";
|
|
pub const AUTH_UPDATE_PASSWORD: &str =
|
|
"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?";
|
|
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, 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, 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, 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, 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, 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 = ?";
|
|
pub const USER_DELETE_PAD: &str = "DELETE FROM pads WHERE slug = ?";
|
|
pub const USER_SET_WORKSPACE_PASSWORD: &str =
|
|
"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?";
|
|
pub const USER_SET_PAD_PASSWORD: &str =
|
|
"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?";
|
|
pub const USER_SET_WORKSPACE_PRIVACY: &str =
|
|
"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?";
|
|
pub const USER_SET_PAD_PRIVACY: &str =
|
|
"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?";
|
|
pub const RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE: &str =
|
|
"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?";
|
|
pub const RESOURCE_ACCESS_TOKENS_INSERT: &str = "INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)";
|
|
pub const RESOURCE_ACCESS_TOKENS_VALID_COUNT: &str = "SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?";
|
|
pub const RESOURCE_PERMISSION_DELETE_USER: &str = "DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?";
|
|
pub const RESOURCE_PERMISSION_INSERT: &str = "INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)";
|
|
pub const SHARE_INVITATION_DELETE_USER: &str = "DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?";
|
|
pub const SHARE_INVITATION_INSERT: &str = "INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
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, 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, 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, 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 = ?";
|
|
pub const RESOURCE_PERMISSION_BY_USER: &str = "SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?";
|
|
pub const SHARE_LINK_PERMISSION: &str = "SELECT permission FROM resource_share_links WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)";
|
|
|
|
// 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, 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, 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, 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 =
|
|
"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
pub const Q007: &str = "UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
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, 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, 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 =
|
|
"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
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, 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, 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 = ?";
|
|
pub const Q025: &str = "UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL";
|
|
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 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 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 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, 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, 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, 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, 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<Mutex<HashMap<&'static str, &'static str>>> = OnceLock::new();
|
|
|
|
pub fn get(kind: DatabaseKind, query: &'static str) -> &'static str {
|
|
let query = match (kind, query) {
|
|
(DatabaseKind::MySql, USER_LIST_WORKSPACES) => USER_LIST_WORKSPACES_MYSQL,
|
|
(DatabaseKind::MySql, USER_LIST_PADS) => USER_LIST_PADS_MYSQL,
|
|
(DatabaseKind::MySql, SHARE_INVITATION_FIND_TOKEN) => SHARE_INVITATION_FIND_TOKEN_MYSQL,
|
|
(DatabaseKind::MySql, RESOURCE_SHARING_LINKS) => RESOURCE_SHARING_LINKS_MYSQL,
|
|
(DatabaseKind::MySql, RESOURCE_SHARING_PENDING) => RESOURCE_SHARING_PENDING_MYSQL,
|
|
(DatabaseKind::MySql, AUTH_ACCOUNT_ACTION_BY_TOKEN) => AUTH_ACCOUNT_ACTION_BY_TOKEN_MYSQL,
|
|
(DatabaseKind::MySql, AUTH_USER_BY_EXTERNAL_ID) => AUTH_USER_BY_EXTERNAL_ID_MYSQL,
|
|
(DatabaseKind::MySql, AUTH_USER_BY_SESSION) => AUTH_USER_BY_SESSION_MYSQL,
|
|
(DatabaseKind::MySql, AUTH_USER_BY_NICKNAME) => AUTH_USER_BY_NICKNAME_MYSQL,
|
|
(DatabaseKind::MySql, AUTH_USER_BY_EMAIL) => AUTH_USER_BY_EMAIL_MYSQL,
|
|
(DatabaseKind::MySql, Q001) => Q001_MYSQL,
|
|
(DatabaseKind::Postgres, Q001) => Q001_POSTGRES,
|
|
(DatabaseKind::MySql, Q003) => Q003_MYSQL,
|
|
(DatabaseKind::MySql, Q004) => Q004_MYSQL,
|
|
(DatabaseKind::MySql, Q010) => Q010_MYSQL,
|
|
(DatabaseKind::MySql, Q011) => Q011_MYSQL,
|
|
(DatabaseKind::Postgres, Q011) => Q011_POSTGRES,
|
|
(DatabaseKind::MySql, Q016) => Q016_MYSQL,
|
|
(DatabaseKind::MySql, Q021) => Q021_MYSQL,
|
|
(DatabaseKind::MySql, Q028) => Q028_MYSQL,
|
|
(DatabaseKind::MySql, Q029) => Q029_MYSQL,
|
|
(DatabaseKind::MySql, Q030) => Q030_MYSQL,
|
|
(DatabaseKind::MySql, Q033) => Q033_MYSQL,
|
|
(DatabaseKind::MySql, Q036) => Q036_MYSQL,
|
|
(DatabaseKind::MySql, Q038) => Q038_MYSQL,
|
|
(DatabaseKind::MySql, Q046) => Q046_MYSQL,
|
|
_ => query,
|
|
};
|
|
|
|
if kind != DatabaseKind::Postgres {
|
|
return query;
|
|
}
|
|
let cache = POSTGRES_QUERIES.get_or_init(|| Mutex::new(HashMap::new()));
|
|
let mut cache = cache.lock().expect("query cache lock poisoned");
|
|
if let Some(value) = cache.get(query) {
|
|
return value;
|
|
}
|
|
let cache_key = query;
|
|
let query = query.replace("CURRENT_TIMESTAMP", "(CURRENT_TIMESTAMP::text)");
|
|
let mut index = 0;
|
|
let mut converted = String::with_capacity(query.len() + 8);
|
|
for ch in query.chars() {
|
|
if ch == '?' {
|
|
index += 1;
|
|
converted.push('$');
|
|
converted.push_str(&index.to_string());
|
|
} else {
|
|
converted.push(ch);
|
|
}
|
|
}
|
|
let converted = Box::leak(converted.into_boxed_str());
|
|
cache.insert(cache_key, converted);
|
|
converted
|
|
}
|
|
|
|
pub const Q040: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?";
|
|
pub const Q041: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?";
|
|
pub const Q042: &str = "UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
pub const Q043: &str = "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
|
|
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 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";
|
|
pub const Q045_POSTGRES: &str = "SELECT allow_task_updates FROM published_pages WHERE note_id = $1";
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn privacy_cast_matches_database_dialect() {
|
|
assert!(get(DatabaseKind::Sqlite, Q001).contains(" AS INTEGER)"));
|
|
assert!(get(DatabaseKind::MySql, Q001).contains(" AS SIGNED)"));
|
|
|
|
let postgres = get(DatabaseKind::Postgres, Q001);
|
|
assert!(postgres.contains(" AS BIGINT)"));
|
|
assert!(postgres.ends_with("slug = $1"));
|
|
}
|
|
|
|
#[test]
|
|
fn pad_privacy_cast_matches_database_dialect() {
|
|
assert!(get(DatabaseKind::Sqlite, Q011).contains(" AS INTEGER)"));
|
|
assert!(get(DatabaseKind::MySql, Q011).contains(" AS SIGNED)"));
|
|
|
|
let postgres = get(DatabaseKind::Postgres, Q011);
|
|
assert!(postgres.contains(" AS BIGINT)"));
|
|
assert!(postgres.ends_with("slug = $1"));
|
|
}
|
|
|
|
#[test]
|
|
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,
|
|
AUTH_USER_BY_SESSION, AUTH_USER_BY_NICKNAME, AUTH_USER_BY_EMAIL,
|
|
USER_LIST_WORKSPACES, USER_LIST_PADS, SHARE_INVITATION_FIND_TOKEN,
|
|
RESOURCE_SHARING_LINKS, RESOURCE_SHARING_PENDING,
|
|
] {
|
|
assert!(
|
|
get(DatabaseKind::MySql, query).contains("CHAR CHARACTER SET utf8mb4"),
|
|
"missing MySQL text cast for query: {query}"
|
|
);
|
|
}
|
|
}
|
|
}
|