diff --git a/Cargo.lock b/Cargo.lock index 0df1cbd..c979b1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.1.7" +version = "0.1.8" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index d3c3f20..4a893ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.1.7" +version = "0.1.8" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/README.md b/README.md index 4b92502..ce33041 100644 --- a/README.md +++ b/README.md @@ -263,3 +263,13 @@ rustpad --config /etc/rustpad/rustpad.yaml ``` `check-config` validates YAML syntax, supported keys, value types, required LDAP/S3/SMTP fields, database URL scheme, paths and dependent settings. It does not connect to the database or LDAP server. Example deployment files are in `systemd/`. + +### Database query layout + +SQL is selected explicitly by database engine. Application code uses logical query identifiers from `src/queries/mod.rs`; complete engine-specific statements live in: + +- `src/queries/sqlite.rs` +- `src/queries/postgres.rs` +- `src/queries/mysql.rs` + +PostgreSQL statements use native `$1`, `$2`, ... placeholders. Query text is not rewritten at runtime, and result-shape casts are defined independently for each engine. diff --git a/src/database.rs b/src/database.rs index 53ad689..f0a003b 100644 --- a/src/database.rs +++ b/src/database.rs @@ -26,13 +26,13 @@ impl Database { .await?; if kind == DatabaseKind::Sqlite { debug!("applying SQLite connection pragmas"); - sqlx::query(queries::SQLITE_FOREIGN_KEYS_ON) + sqlx::query(queries::get(kind, queries::SQLITE_FOREIGN_KEYS_ON)) .execute(&pool) .await?; - sqlx::query(queries::SQLITE_JOURNAL_WAL) + sqlx::query(queries::get(kind, queries::SQLITE_JOURNAL_WAL)) .execute(&pool) .await?; - sqlx::query(queries::SQLITE_BUSY_TIMEOUT) + sqlx::query(queries::get(kind, queries::SQLITE_BUSY_TIMEOUT)) .execute(&pool) .await?; } diff --git a/src/db.rs b/src/db.rs index 651a549..1aa4e85 100644 --- a/src/db.rs +++ b/src/db.rs @@ -15,11 +15,11 @@ async fn inserted_id( table: &str, ) -> Result { let query = match kind { - DatabaseKind::Sqlite => queries::SQLITE_LAST_INSERT_ID, - DatabaseKind::MySql => queries::MYSQL_LAST_INSERT_ID, + DatabaseKind::Sqlite => queries::get(kind, queries::SQLITE_LAST_INSERT_ID), + DatabaseKind::MySql => queries::get(kind, queries::MYSQL_LAST_INSERT_ID), DatabaseKind::Postgres => match table { - "note_revisions" => queries::POSTGRES_NOTE_REVISION_LAST_INSERT_ID, - "revisions" => queries::POSTGRES_PAD_REVISION_LAST_INSERT_ID, + "note_revisions" => queries::get(kind, queries::POSTGRES_NOTE_REVISION_LAST_INSERT_ID), + "revisions" => queries::get(kind, queries::POSTGRES_PAD_REVISION_LAST_INSERT_ID), _ => unreachable!("unsupported identity table"), }, }; @@ -142,7 +142,7 @@ pub fn verify_workspace_password(workspace: &Workspace, password: Option<&str>) pub async fn list_notes(pool: &Database, workspace_id: i64) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { - return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q003) + return Ok(sqlx::query_as::<_, SqliteNote>(queries::get(pool.kind(), queries::Q003)) .bind(workspace_id) .fetch_all(pool.pool()) .await? @@ -162,7 +162,7 @@ pub async fn find_note( slug: &str, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { - return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q004) + return Ok(sqlx::query_as::<_, SqliteNote>(queries::get(pool.kind(), queries::Q004)) .bind(workspace_id) .bind(slug) .fetch_optional(pool.pool()) @@ -500,7 +500,7 @@ pub async fn find_published_page( ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Postgres { return Ok( - sqlx::query_as::<_, PostgresPublishedPageRow>(queries::Q021_POSTGRES) + sqlx::query_as::<_, PostgresPublishedPageRow>(queries::get(pool.kind(), queries::Q021)) .bind(token) .fetch_optional(pool.pool()) .await? @@ -518,7 +518,7 @@ pub async fn find_published_page( pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result { if pool.kind() == DatabaseKind::Postgres { - return Ok(sqlx::query_scalar::<_, bool>(queries::Q044_POSTGRES) + return Ok(sqlx::query_scalar::<_, bool>(queries::get(pool.kind(), queries::Q044)) .bind(pad_id) .fetch_optional(pool.pool()) .await? @@ -534,7 +534,7 @@ pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result Result { if pool.kind() == DatabaseKind::Postgres { - return Ok(sqlx::query_scalar::<_, bool>(queries::Q045_POSTGRES) + return Ok(sqlx::query_scalar::<_, bool>(queries::get(pool.kind(), queries::Q045)) .bind(note_id) .fetch_optional(pool.pool()) .await? @@ -790,11 +790,11 @@ pub async fn list_note_files(pool: &Database, note_id: i64) -> Result Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { - return Ok(sqlx::query_as::<_, SqliteNoteFile>(query) + return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::get(pool.kind(), query)) .bind(owner_id) .fetch_all(pool.pool()) .await? @@ -875,7 +875,7 @@ pub async fn find_note_file( file_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { - return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::Q038) + return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::get(pool.kind(), queries::Q038)) .bind(file_id) .bind(note_id) .fetch_optional(pool.pool()) @@ -908,7 +908,7 @@ pub async fn find_pad_file( file_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { - return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::Q046) + return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::get(pool.kind(), queries::Q046)) .bind(file_id) .bind(pad_id) .fetch_optional(pool.pool()) diff --git a/src/queries.rs b/src/queries.rs deleted file mode 100644 index 865afaf..0000000 --- a/src/queries.rs +++ /dev/null @@ -1,309 +0,0 @@ -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, 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, 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 = - "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, 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 = ?"; -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, 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, 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, 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, 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(); - -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, - (DatabaseKind::MySql, Q044) => Q044_MYSQL, - (DatabaseKind::MySql, Q045) => Q045_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 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"; -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}" - ); - } - } -} diff --git a/src/queries/mod.rs b/src/queries/mod.rs new file mode 100644 index 0000000..6f144e0 --- /dev/null +++ b/src/queries/mod.rs @@ -0,0 +1,274 @@ +use crate::database::DatabaseKind; + +mod mysql; +mod postgres; +mod sqlite; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(non_camel_case_types)] +pub enum Query { + SQLITE_FOREIGN_KEYS_ON, + SQLITE_JOURNAL_WAL, + SQLITE_BUSY_TIMEOUT, + SQLITE_LAST_INSERT_ID, + MYSQL_LAST_INSERT_ID, + POSTGRES_NOTE_REVISION_LAST_INSERT_ID, + POSTGRES_PAD_REVISION_LAST_INSERT_ID, + AUTH_LATEST_CONFIRMATION_CREATED_AT, + AUTH_UPDATE_NICKNAME, + AUTH_UPDATE_EDITOR_COLOR, + AUTH_EDITOR_COLOR_BY_USER, + RESOURCE_COLOR_BY_USER, + RESOURCE_COLOR_DELETE, + RESOURCE_COLOR_INSERT, + AUTH_ACCOUNT_ACTION_BY_TOKEN, + AUTH_CONSUME_ACCOUNT_ACTION, + AUTH_UPDATE_EMAIL, + AUTH_DELETE_ACCOUNT_ACTIONS, + AUTH_INSERT_ACCOUNT_ACTION, + AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS, + AUTH_INSERT_USER, + AUTH_INSERT_DIRECTORY_USER, + AUTH_UPDATE_DIRECTORY_USER, + AUTH_DIRECTORY_PROFILE_BY_USER, + AUTH_USER_BY_EXTERNAL_ID, + AUTH_DELETE_USER, + AUTH_SESSION_EXPIRES_AT, + AUTH_DELETE_SESSION_BY_TOKEN, + AUTH_REFRESH_SESSION, + AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER, + AUTH_INSERT_CONFIRMATION_TOKEN, + AUTH_FIND_CONFIRMATION_TOKEN, + AUTH_CONFIRM_USER, + AUTH_CONSUME_CONFIRMATION_TOKEN, + AUTH_DELETE_RESET_TOKENS_BY_USER, + AUTH_INSERT_RESET_TOKEN, + AUTH_FIND_RESET_TOKEN, + AUTH_UPDATE_PASSWORD, + AUTH_CONSUME_RESET_TOKEN, + AUTH_DELETE_SESSIONS_BY_USER, + AUTH_USER_BY_SESSION, + AUTH_INSERT_SESSION, + AUTH_USER_BY_NICKNAME, + AUTH_USER_BY_EMAIL, + USER_ATTACH_WORKSPACE, + USER_ATTACH_PAD, + USER_LIST_WORKSPACES, + USER_LIST_PADS, + USER_OWNS_WORKSPACE, + USER_OWNS_PAD, + USER_DELETE_WORKSPACE, + USER_DELETE_PAD, + USER_SET_WORKSPACE_PASSWORD, + USER_SET_PAD_PASSWORD, + USER_SET_WORKSPACE_PRIVACY, + USER_SET_PAD_PRIVACY, + RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE, + RESOURCE_ACCESS_TOKENS_INSERT, + RESOURCE_ACCESS_TOKENS_VALID_COUNT, + RESOURCE_PERMISSION_DELETE_USER, + RESOURCE_PERMISSION_INSERT, + SHARE_INVITATION_DELETE_USER, + SHARE_INVITATION_INSERT, + SHARE_INVITATION_DELETE_TOKEN, + SHARE_INVITATION_FIND_TOKEN, + SHARE_INVITATION_ACCEPT, + RESOURCE_SHARING_USERS, + RESOURCE_SHARING_LINKS, + RESOURCE_SHARING_PENDING, + SHARE_LINK_INSERT, + SHARE_LINK_UPDATE, + SHARE_LINK_REVOKE, + RESOURCE_PERMISSION_BY_USER, + SHARE_LINK_PERMISSION, + Q001, + Q002, + Q003, + Q004, + Q005, + Q006, + Q007, + Q008, + Q009, + Q010, + Q011, + Q012, + Q013, + Q014, + Q015, + Q016, + Q017, + Q018, + Q019, + Q020, + Q021, + Q022, + Q023, + Q024, + Q025, + Q026, + Q027, + Q028, + Q029, + Q030, + Q031, + Q032, + Q033, + Q034, + Q035, + Q036, + Q037, + Q038, + Q039, + Q046, + Q047, + Q040, + Q041, + Q042, + Q043, + Q044, + Q045, +} + +pub fn get(kind: DatabaseKind, query: Query) -> &'static str { + match kind { + DatabaseKind::Sqlite => sqlite::get(query), + DatabaseKind::Postgres => postgres::get(query), + DatabaseKind::MySql => mysql::get(query), + } +} + +pub const SQLITE_FOREIGN_KEYS_ON: Query = Query::SQLITE_FOREIGN_KEYS_ON; +pub const SQLITE_JOURNAL_WAL: Query = Query::SQLITE_JOURNAL_WAL; +pub const SQLITE_BUSY_TIMEOUT: Query = Query::SQLITE_BUSY_TIMEOUT; +pub const SQLITE_LAST_INSERT_ID: Query = Query::SQLITE_LAST_INSERT_ID; +pub const MYSQL_LAST_INSERT_ID: Query = Query::MYSQL_LAST_INSERT_ID; +pub const POSTGRES_NOTE_REVISION_LAST_INSERT_ID: Query = Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID; +pub const POSTGRES_PAD_REVISION_LAST_INSERT_ID: Query = Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID; +pub const AUTH_LATEST_CONFIRMATION_CREATED_AT: Query = Query::AUTH_LATEST_CONFIRMATION_CREATED_AT; +pub const AUTH_UPDATE_NICKNAME: Query = Query::AUTH_UPDATE_NICKNAME; +pub const AUTH_UPDATE_EDITOR_COLOR: Query = Query::AUTH_UPDATE_EDITOR_COLOR; +pub const AUTH_EDITOR_COLOR_BY_USER: Query = Query::AUTH_EDITOR_COLOR_BY_USER; +pub const RESOURCE_COLOR_BY_USER: Query = Query::RESOURCE_COLOR_BY_USER; +pub const RESOURCE_COLOR_DELETE: Query = Query::RESOURCE_COLOR_DELETE; +pub const RESOURCE_COLOR_INSERT: Query = Query::RESOURCE_COLOR_INSERT; +pub const AUTH_ACCOUNT_ACTION_BY_TOKEN: Query = Query::AUTH_ACCOUNT_ACTION_BY_TOKEN; +pub const AUTH_CONSUME_ACCOUNT_ACTION: Query = Query::AUTH_CONSUME_ACCOUNT_ACTION; +pub const AUTH_UPDATE_EMAIL: Query = Query::AUTH_UPDATE_EMAIL; +pub const AUTH_DELETE_ACCOUNT_ACTIONS: Query = Query::AUTH_DELETE_ACCOUNT_ACTIONS; +pub const AUTH_INSERT_ACCOUNT_ACTION: Query = Query::AUTH_INSERT_ACCOUNT_ACTION; +pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: Query = Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS; +pub const AUTH_INSERT_USER: Query = Query::AUTH_INSERT_USER; +pub const AUTH_INSERT_DIRECTORY_USER: Query = Query::AUTH_INSERT_DIRECTORY_USER; +pub const AUTH_UPDATE_DIRECTORY_USER: Query = Query::AUTH_UPDATE_DIRECTORY_USER; +pub const AUTH_DIRECTORY_PROFILE_BY_USER: Query = Query::AUTH_DIRECTORY_PROFILE_BY_USER; +pub const AUTH_USER_BY_EXTERNAL_ID: Query = Query::AUTH_USER_BY_EXTERNAL_ID; +pub const AUTH_DELETE_USER: Query = Query::AUTH_DELETE_USER; +pub const AUTH_SESSION_EXPIRES_AT: Query = Query::AUTH_SESSION_EXPIRES_AT; +pub const AUTH_DELETE_SESSION_BY_TOKEN: Query = Query::AUTH_DELETE_SESSION_BY_TOKEN; +pub const AUTH_REFRESH_SESSION: Query = Query::AUTH_REFRESH_SESSION; +pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: Query = Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER; +pub const AUTH_INSERT_CONFIRMATION_TOKEN: Query = Query::AUTH_INSERT_CONFIRMATION_TOKEN; +pub const AUTH_FIND_CONFIRMATION_TOKEN: Query = Query::AUTH_FIND_CONFIRMATION_TOKEN; +pub const AUTH_CONFIRM_USER: Query = Query::AUTH_CONFIRM_USER; +pub const AUTH_CONSUME_CONFIRMATION_TOKEN: Query = Query::AUTH_CONSUME_CONFIRMATION_TOKEN; +pub const AUTH_DELETE_RESET_TOKENS_BY_USER: Query = Query::AUTH_DELETE_RESET_TOKENS_BY_USER; +pub const AUTH_INSERT_RESET_TOKEN: Query = Query::AUTH_INSERT_RESET_TOKEN; +pub const AUTH_FIND_RESET_TOKEN: Query = Query::AUTH_FIND_RESET_TOKEN; +pub const AUTH_UPDATE_PASSWORD: Query = Query::AUTH_UPDATE_PASSWORD; +pub const AUTH_CONSUME_RESET_TOKEN: Query = Query::AUTH_CONSUME_RESET_TOKEN; +pub const AUTH_DELETE_SESSIONS_BY_USER: Query = Query::AUTH_DELETE_SESSIONS_BY_USER; +pub const AUTH_USER_BY_SESSION: Query = Query::AUTH_USER_BY_SESSION; +pub const AUTH_INSERT_SESSION: Query = Query::AUTH_INSERT_SESSION; +pub const AUTH_USER_BY_NICKNAME: Query = Query::AUTH_USER_BY_NICKNAME; +pub const AUTH_USER_BY_EMAIL: Query = Query::AUTH_USER_BY_EMAIL; +pub const USER_ATTACH_WORKSPACE: Query = Query::USER_ATTACH_WORKSPACE; +pub const USER_ATTACH_PAD: Query = Query::USER_ATTACH_PAD; +pub const USER_LIST_WORKSPACES: Query = Query::USER_LIST_WORKSPACES; +pub const USER_LIST_PADS: Query = Query::USER_LIST_PADS; +pub const USER_OWNS_WORKSPACE: Query = Query::USER_OWNS_WORKSPACE; +pub const USER_OWNS_PAD: Query = Query::USER_OWNS_PAD; +pub const USER_DELETE_WORKSPACE: Query = Query::USER_DELETE_WORKSPACE; +pub const USER_DELETE_PAD: Query = Query::USER_DELETE_PAD; +pub const USER_SET_WORKSPACE_PASSWORD: Query = Query::USER_SET_WORKSPACE_PASSWORD; +pub const USER_SET_PAD_PASSWORD: Query = Query::USER_SET_PAD_PASSWORD; +pub const USER_SET_WORKSPACE_PRIVACY: Query = Query::USER_SET_WORKSPACE_PRIVACY; +pub const USER_SET_PAD_PRIVACY: Query = Query::USER_SET_PAD_PRIVACY; +pub const RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE: Query = Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE; +pub const RESOURCE_ACCESS_TOKENS_INSERT: Query = Query::RESOURCE_ACCESS_TOKENS_INSERT; +pub const RESOURCE_ACCESS_TOKENS_VALID_COUNT: Query = Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT; +pub const RESOURCE_PERMISSION_DELETE_USER: Query = Query::RESOURCE_PERMISSION_DELETE_USER; +pub const RESOURCE_PERMISSION_INSERT: Query = Query::RESOURCE_PERMISSION_INSERT; +pub const SHARE_INVITATION_DELETE_USER: Query = Query::SHARE_INVITATION_DELETE_USER; +pub const SHARE_INVITATION_INSERT: Query = Query::SHARE_INVITATION_INSERT; +pub const SHARE_INVITATION_DELETE_TOKEN: Query = Query::SHARE_INVITATION_DELETE_TOKEN; +pub const SHARE_INVITATION_FIND_TOKEN: Query = Query::SHARE_INVITATION_FIND_TOKEN; +pub const SHARE_INVITATION_ACCEPT: Query = Query::SHARE_INVITATION_ACCEPT; +pub const RESOURCE_SHARING_USERS: Query = Query::RESOURCE_SHARING_USERS; +pub const RESOURCE_SHARING_LINKS: Query = Query::RESOURCE_SHARING_LINKS; +pub const RESOURCE_SHARING_PENDING: Query = Query::RESOURCE_SHARING_PENDING; +pub const SHARE_LINK_INSERT: Query = Query::SHARE_LINK_INSERT; +pub const SHARE_LINK_UPDATE: Query = Query::SHARE_LINK_UPDATE; +pub const SHARE_LINK_REVOKE: Query = Query::SHARE_LINK_REVOKE; +pub const RESOURCE_PERMISSION_BY_USER: Query = Query::RESOURCE_PERMISSION_BY_USER; +pub const SHARE_LINK_PERMISSION: Query = Query::SHARE_LINK_PERMISSION; +pub const Q001: Query = Query::Q001; +pub const Q002: Query = Query::Q002; +pub const Q003: Query = Query::Q003; +pub const Q004: Query = Query::Q004; +pub const Q005: Query = Query::Q005; +pub const Q006: Query = Query::Q006; +pub const Q007: Query = Query::Q007; +pub const Q008: Query = Query::Q008; +pub const Q009: Query = Query::Q009; +pub const Q010: Query = Query::Q010; +pub const Q011: Query = Query::Q011; +pub const Q012: Query = Query::Q012; +pub const Q013: Query = Query::Q013; +pub const Q014: Query = Query::Q014; +pub const Q015: Query = Query::Q015; +pub const Q016: Query = Query::Q016; +pub const Q017: Query = Query::Q017; +pub const Q018: Query = Query::Q018; +pub const Q019: Query = Query::Q019; +pub const Q020: Query = Query::Q020; +pub const Q021: Query = Query::Q021; +pub const Q022: Query = Query::Q022; +pub const Q023: Query = Query::Q023; +pub const Q024: Query = Query::Q024; +pub const Q025: Query = Query::Q025; +pub const Q026: Query = Query::Q026; +pub const Q027: Query = Query::Q027; +pub const Q028: Query = Query::Q028; +pub const Q029: Query = Query::Q029; +pub const Q030: Query = Query::Q030; +pub const Q031: Query = Query::Q031; +pub const Q032: Query = Query::Q032; +pub const Q033: Query = Query::Q033; +pub const Q034: Query = Query::Q034; +pub const Q035: Query = Query::Q035; +pub const Q036: Query = Query::Q036; +pub const Q037: Query = Query::Q037; +pub const Q038: Query = Query::Q038; +pub const Q039: Query = Query::Q039; +pub const Q046: Query = Query::Q046; +pub const Q047: Query = Query::Q047; +pub const Q040: Query = Query::Q040; +pub const Q041: Query = Query::Q041; +pub const Q042: Query = Query::Q042; +pub const Q043: Query = Query::Q043; +pub const Q044: Query = Query::Q044; +pub const Q045: Query = Query::Q045; + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn every_backend_has_explicit_queries() { + for query in [Q001, Q003, Q004, Q011, Q021, Q033, USER_LIST_WORKSPACES, USER_LIST_PADS] { + assert!(!get(DatabaseKind::Sqlite, query).is_empty()); + assert!(!get(DatabaseKind::Postgres, query).is_empty()); + assert!(!get(DatabaseKind::MySql, query).is_empty()); + } + } +} diff --git a/src/queries/mysql.rs b/src/queries/mysql.rs new file mode 100644 index 0000000..2d47d89 --- /dev/null +++ b/src/queries/mysql.rs @@ -0,0 +1,127 @@ +use super::Query; + +pub fn get(query: Query) -> &'static str { + match query { + Query::SQLITE_FOREIGN_KEYS_ON => r#"PRAGMA foreign_keys = ON"#, + Query::SQLITE_JOURNAL_WAL => r#"PRAGMA journal_mode = WAL"#, + Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, + Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, + Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"#, + Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, + Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, + Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, + Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"#, + Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"#, + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"#, + Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES (?, ?, ?, ?, ?, ?)"#, + Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, + Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#, + Query::AUTH_USER_BY_EXTERNAL_ID => r#"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 = ?"#, + Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#, + Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#, + Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#, + Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#, + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"#, + Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"#, + Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = ?"#, + Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"#, + Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, + Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#, + Query::AUTH_USER_BY_SESSION => r#"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 > ?"#, + Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE nickname_key = ?"#, + Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE email_key = ?"#, + Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#, + Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT ?, id FROM pads WHERE slug = ?"#, + Query::USER_LIST_WORKSPACES => r#"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"#, + Query::USER_LIST_PADS => r#"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"#, + Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"#, + Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"#, + Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = ?"#, + Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"#, + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"#, + Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"#, + Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"#, + Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"#, + Query::SHARE_INVITATION_FIND_TOKEN => r#"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 = ?"#, + Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"#, + Query::RESOURCE_SHARING_USERS => r#"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"#, + Query::RESOURCE_SHARING_LINKS => r#"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"#, + Query::RESOURCE_SHARING_PENDING => r#"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"#, + Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#, + Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#, + Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::SHARE_LINK_PERMISSION => r#"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 > ?)"#, + Query::Q001 => r#"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 = ?"#, + Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)"#, + Query::Q003 => r#"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"#, + Query::Q004 => r#"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 = ?"#, + Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"#, + Query::Q006 => r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q007 => r#"UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = ?"#, + Query::Q010 => r#"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"#, + Query::Q011 => r#"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 = ?"#, + Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)"#, + Query::Q013 => r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = ?"#, + Query::Q016 => r#"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"#, + Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = ?"#, + Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES (?, ?)"#, + Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = ?"#, + Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES (?, ?)"#, + Query::Q021 => r#"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 = ?"#, + Query::Q022 => r#"SELECT file_token FROM pads WHERE id = ?"#, + Query::Q023 => r#"UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL"#, + Query::Q024 => r#"SELECT file_token FROM notes WHERE id = ?"#, + Query::Q025 => r#"UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL"#, + Query::Q026 => r#"SELECT id FROM pads WHERE file_token = ?"#, + Query::Q027 => r#"SELECT id FROM notes WHERE file_token = ?"#, + Query::Q028 => r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM note_revisions WHERE id = ? AND note_id = ?"#, + Query::Q029 => r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"#, + Query::Q030 => r#"SELECT CAST(owner_map AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"#, + Query::Q031 => r#"DELETE FROM notes WHERE id = ?"#, + Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, + Query::Q033 => r#"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"#, + Query::Q034 => r#"UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, + Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, + Query::Q036 => r#"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"#, + Query::Q037 => r#"UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, + Query::Q038 => r#"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 = ?"#, + Query::Q039 => r#"DELETE FROM note_files WHERE id = ? AND note_id = ?"#, + Query::Q046 => r#"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 = ?"#, + Query::Q047 => r#"DELETE FROM pad_files WHERE id = ? AND pad_id = ?"#, + Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?"#, + Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?"#, + Query::Q042 => r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q043 => r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q044 => r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE pad_id = ?"#, + Query::Q045 => r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE note_id = ?"#, + } +} diff --git a/src/queries/postgres.rs b/src/queries/postgres.rs new file mode 100644 index 0000000..2a974cb --- /dev/null +++ b/src/queries/postgres.rs @@ -0,0 +1,127 @@ +use super::Query; + +pub fn get(query: Query) -> &'static str { + match query { + Query::SQLITE_FOREIGN_KEYS_ON => r#"PRAGMA foreign_keys = ON"#, + Query::SQLITE_JOURNAL_WAL => r#"PRAGMA journal_mode = WAL"#, + Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, + Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, + Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1"#, + Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = $1, nickname_key = $2, updated_at = $3 WHERE id = $4"#, + Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = $1, updated_at = $2 WHERE id = $3"#, + Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = $1"#, + Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, + Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, + Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES ($1, $2, $3, $4)"#, + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = $1"#, + Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL"#, + Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = $1, email_key = $2, updated_at = $3 WHERE id = $4"#, + Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = $1 AND action = $2"#, + Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES ($1, $2, $3, $4, $5, $6)"#, + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < $1"#, + Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES ($1, $2, $3, $4, $5, $6)"#, + Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"#, + Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = $1, nickname_key = $2, email = $3, email_key = $4, auth_provider = $5, external_id = $6, external_dn = $7, directory_display_name = $8, updated_at = $9 WHERE id = $10"#, + Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = $1"#, + Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = $1 AND external_id = $2"#, + Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = $1"#, + Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = $1"#, + Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = $1"#, + Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = $1 WHERE token = $2 AND expires_at > $3"#, + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = $1"#, + Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"#, + Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = $1"#, + Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = $1, updated_at = $2 WHERE id = $3"#, + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"#, + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = $1"#, + Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"#, + Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = $1"#, + Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = $1, updated_at = $2 WHERE id = $3"#, + Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"#, + Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = $1"#, + Query::AUTH_USER_BY_SESSION => r#"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 = $1 AND s.expires_at > $2"#, + Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES ($1, $2, $3)"#, + Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE nickname_key = $1"#, + Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE email_key = $1"#, + Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT $1, id FROM workspaces WHERE slug = $2"#, + Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT $1, id FROM pads WHERE slug = $2"#, + Query::USER_LIST_WORKSPACES => r#"SELECT w.slug, w.title, CAST(CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT) AS protected, w.updated_at, CAST(CASE WHEN w.is_private THEN 1 ELSE 0 END AS BIGINT) AS private, CAST(1 AS BIGINT) 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 = $1 UNION SELECT w.slug, w.title, CAST(CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT), w.updated_at, CAST(CASE WHEN w.is_private THEN 1 ELSE 0 END AS BIGINT), CAST(0 AS BIGINT), 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 = $2 ORDER BY updated_at DESC"#, + Query::USER_LIST_PADS => r#"SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT) AS protected, p.updated_at, CAST(CASE WHEN p.is_private THEN 1 ELSE 0 END AS BIGINT) AS private, CAST(1 AS BIGINT) 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 = $1 UNION SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT), p.updated_at, CAST(CASE WHEN p.is_private THEN 1 ELSE 0 END AS BIGINT), CAST(0 AS BIGINT), 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 = $2 ORDER BY updated_at DESC"#, + Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = $1 AND w.slug = $2"#, + Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = $1 AND p.slug = $2"#, + Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = $1"#, + Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = $1"#, + Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, + Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, + Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, + Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = $1 AND resource_slug = $2"#, + Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES ($1, $2, $3, $4)"#, + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND expires_at > $4"#, + Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, + Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES ($1, $2, $3, $4)"#, + Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, + Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7)"#, + Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = $1"#, + Query::SHARE_INVITATION_FIND_TOKEN => r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = $1"#, + Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = $1 WHERE token_hash = $2"#, + Query::RESOURCE_SHARING_USERS => r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = $1 AND rp.resource_slug = $2 ORDER BY u.email"#, + Query::RESOURCE_SHARING_LINKS => r#"SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = $1 AND resource_slug = $2 AND revoked_at IS NULL ORDER BY created_at DESC"#, + Query::RESOURCE_SHARING_PENDING => r#"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 = $1 AND i.resource_slug = $2 AND i.accepted_at IS NULL ORDER BY u.email"#, + Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)"#, + Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = $1, expires_at = $2 WHERE token_hash = $3 AND resource_kind = $4 AND resource_slug = $5 AND revoked_at IS NULL"#, + Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = $1 WHERE token_hash = $2 AND resource_kind = $3 AND resource_slug = $4"#, + Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, + Query::SHARE_LINK_PERMISSION => r#"SELECT permission FROM resource_share_links WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > $4)"#, + Query::Q001 => r#"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 = $1"#, + Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES ($1, $2, $3)"#, + Query::Q003 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 ORDER BY updated_at DESC, id DESC"#, + Query::Q004 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 AND slug = $2"#, + Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES ($1, $2, $3, $4, $5)"#, + Query::Q006 => r#"UPDATE notes SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"#, + Query::Q007 => r#"UPDATE workspaces SET updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $1"#, + Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"#, + Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = $1"#, + Query::Q010 => r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = $1 ORDER BY id DESC LIMIT 100"#, + Query::Q011 => r#"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 = $1"#, + Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES ($1, $2, $3)"#, + Query::Q013 => r#"UPDATE pads SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"#, + Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"#, + Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = $1"#, + Query::Q016 => r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = $1 ORDER BY id DESC LIMIT 100"#, + Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = $1"#, + Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES ($1, $2)"#, + Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = $1"#, + Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES ($1, $2)"#, + Query::Q021 => r#"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"#, + Query::Q022 => r#"SELECT file_token FROM pads WHERE id = $1"#, + Query::Q023 => r#"UPDATE pads SET file_token = $1 WHERE id = $2 AND file_token IS NULL"#, + Query::Q024 => r#"SELECT file_token FROM notes WHERE id = $1"#, + Query::Q025 => r#"UPDATE notes SET file_token = $1 WHERE id = $2 AND file_token IS NULL"#, + Query::Q026 => r#"SELECT id FROM pads WHERE file_token = $1"#, + Query::Q027 => r#"SELECT id FROM notes WHERE file_token = $1"#, + Query::Q028 => r#"SELECT content FROM note_revisions WHERE id = $1 AND note_id = $2"#, + Query::Q029 => r#"SELECT content FROM revisions WHERE id = $1 AND pad_id = $2"#, + Query::Q030 => r#"SELECT owner_map FROM revisions WHERE id = $1 AND pad_id = $2"#, + Query::Q031 => r#"DELETE FROM notes WHERE id = $1"#, + Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"#, + Query::Q033 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = $1 ORDER BY id DESC"#, + Query::Q034 => r#"UPDATE note_files SET is_attached = $1, detached_at = $2 WHERE id = $3"#, + Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"#, + Query::Q036 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = $1 ORDER BY id DESC"#, + Query::Q037 => r#"UPDATE pad_files SET is_attached = $1, detached_at = $2 WHERE id = $3"#, + Query::Q038 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = $1 AND note_id = $2"#, + Query::Q039 => r#"DELETE FROM note_files WHERE id = $1 AND note_id = $2"#, + Query::Q046 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = $1 AND pad_id = $2"#, + Query::Q047 => r#"DELETE FROM pad_files WHERE id = $1 AND pad_id = $2"#, + Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = $1 WHERE pad_id = $2"#, + Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = $1 WHERE note_id = $2"#, + Query::Q042 => r#"UPDATE pads SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"#, + Query::Q043 => r#"UPDATE notes SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"#, + Query::Q044 => r#"SELECT allow_task_updates FROM published_pages WHERE pad_id = $1"#, + Query::Q045 => r#"SELECT allow_task_updates FROM published_pages WHERE note_id = $1"#, + } +} diff --git a/src/queries/sqlite.rs b/src/queries/sqlite.rs new file mode 100644 index 0000000..1c0b78a --- /dev/null +++ b/src/queries/sqlite.rs @@ -0,0 +1,127 @@ +use super::Query; + +pub fn get(query: Query) -> &'static str { + match query { + Query::SQLITE_FOREIGN_KEYS_ON => r#"PRAGMA foreign_keys = ON"#, + Query::SQLITE_JOURNAL_WAL => r#"PRAGMA journal_mode = WAL"#, + Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, + Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, + Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"#, + Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, + Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, + Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, + Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"#, + Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"#, + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"#, + Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES (?, ?, ?, ?, ?, ?)"#, + Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, + Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#, + Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"#, + Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#, + Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#, + Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#, + Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#, + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"#, + Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"#, + Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = ?"#, + Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"#, + Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#, + Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, + Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#, + Query::AUTH_USER_BY_SESSION => r#"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 > ?"#, + Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#, + Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE nickname_key = ?"#, + Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE email_key = ?"#, + Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#, + Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT ?, id FROM pads WHERE slug = ?"#, + Query::USER_LIST_WORKSPACES => r#"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"#, + Query::USER_LIST_PADS => r#"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"#, + Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"#, + Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"#, + Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = ?"#, + Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"#, + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"#, + Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"#, + Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"#, + Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"#, + Query::SHARE_INVITATION_FIND_TOKEN => r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = ?"#, + Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"#, + Query::RESOURCE_SHARING_USERS => r#"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"#, + Query::RESOURCE_SHARING_LINKS => r#"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"#, + Query::RESOURCE_SHARING_PENDING => r#"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"#, + Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#, + Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#, + Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#, + Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, + Query::SHARE_LINK_PERMISSION => r#"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 > ?)"#, + Query::Q001 => r#"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 = ?"#, + Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)"#, + Query::Q003 => r#"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"#, + Query::Q004 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? AND slug = ?"#, + Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"#, + Query::Q006 => r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q007 => r#"UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = ?"#, + Query::Q010 => r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"#, + Query::Q011 => r#"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 = ?"#, + Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)"#, + Query::Q013 => r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = ?"#, + Query::Q016 => r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"#, + Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = ?"#, + Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES (?, ?)"#, + Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = ?"#, + Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES (?, ?)"#, + Query::Q021 => r#"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 = ?"#, + Query::Q022 => r#"SELECT file_token FROM pads WHERE id = ?"#, + Query::Q023 => r#"UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL"#, + Query::Q024 => r#"SELECT file_token FROM notes WHERE id = ?"#, + Query::Q025 => r#"UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL"#, + Query::Q026 => r#"SELECT id FROM pads WHERE file_token = ?"#, + Query::Q027 => r#"SELECT id FROM notes WHERE file_token = ?"#, + Query::Q028 => r#"SELECT content FROM note_revisions WHERE id = ? AND note_id = ?"#, + Query::Q029 => r#"SELECT content FROM revisions WHERE id = ? AND pad_id = ?"#, + Query::Q030 => r#"SELECT owner_map FROM revisions WHERE id = ? AND pad_id = ?"#, + Query::Q031 => r#"DELETE FROM notes WHERE id = ?"#, + Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, + Query::Q033 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"#, + Query::Q034 => r#"UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, + Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, + Query::Q036 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"#, + Query::Q037 => r#"UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, + Query::Q038 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"#, + Query::Q039 => r#"DELETE FROM note_files WHERE id = ? AND note_id = ?"#, + Query::Q046 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"#, + Query::Q047 => r#"DELETE FROM pad_files WHERE id = ? AND pad_id = ?"#, + Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?"#, + Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?"#, + Query::Q042 => r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q043 => r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q044 => r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?"#, + Query::Q045 => r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?"#, + } +}