From 01ad42f6a18c63801d637231ea7f14ef8b59d2c7 Mon Sep 17 00:00:00 2001 From: Gaumit Kauts <123269559+Gaumit-Kauts@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:29:15 -0700 Subject: [PATCH] Update schema.sql --- schema.sql | 328 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 195 insertions(+), 133 deletions(-) diff --git a/schema.sql b/schema.sql index bc3b668..5193676 100644 --- a/schema.sql +++ b/schema.sql @@ -1,158 +1,217 @@ --- ============================================ --- ARCHIVAL AUDIO SOCIAL MEDIA APP - DATABASE SCHEMA --- ============================================ --- Users Table +CREATE OR REPLACE FUNCTION set_updated_at() +RETURNS trigger AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- ---------- Users ---------- CREATE TABLE users ( - user_id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(50) UNIQUE NOT NULL, - email VARCHAR(100) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - display_name VARCHAR(100), - profile_image_url VARCHAR(255), - bio TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT TRUE, - INDEX idx_username (username), - INDEX idx_email (email) + user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + display_name VARCHAR(100), + profile_image_url VARCHAR(255), + bio TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + is_active BOOLEAN NOT NULL DEFAULT TRUE ); --- Categories Table +-- Note: UNIQUE already creates indexes for username/email, so extra indexes are usually redundant. + +CREATE TRIGGER users_set_updated_at +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE FUNCTION set_updated_at(); + +-- ---------- Categories ---------- CREATE TABLE categories ( - category_id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(100) NOT NULL, - description TEXT, - parent_category_id INT NULL, -- For subcategories - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (parent_category_id) REFERENCES categories(category_id) ON DELETE SET NULL, - INDEX idx_name (name) + category_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + parent_category_id BIGINT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT fk_parent_category + FOREIGN KEY (parent_category_id) + REFERENCES categories(category_id) + ON DELETE SET NULL ); --- Posts Table +CREATE INDEX idx_categories_name ON categories (name); +CREATE INDEX idx_categories_parent ON categories (parent_category_id); + +-- ---------- Posts ---------- CREATE TABLE posts ( - post_id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - title VARCHAR(255), - transcribed_text TEXT, -- Text generated from audio - audio_url VARCHAR(255) NOT NULL, -- URL to stored audio file - audio_duration_seconds INT, -- Duration in seconds - image_url VARCHAR(255), -- Optional image - is_private BOOLEAN DEFAULT FALSE, -- TRUE = private, FALSE = public - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_created_at (created_at), - INDEX idx_is_private (is_private), - FULLTEXT idx_fulltext_search (title, transcribed_text) + post_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL, + title VARCHAR(255), + transcribed_text TEXT, + audio_url VARCHAR(255) NOT NULL, + audio_duration_seconds INT, + image_url VARCHAR(255), + is_private BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + -- Postgres full-text search column (stored generated) + search_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('english', coalesce(title,'') || ' ' || coalesce(transcribed_text,'')) + ) STORED, + + CONSTRAINT fk_posts_user + FOREIGN KEY (user_id) + REFERENCES users(user_id) + ON DELETE CASCADE ); --- Post Categories (Many-to-Many relationship) +CREATE TRIGGER posts_set_updated_at +BEFORE UPDATE ON posts +FOR EACH ROW +EXECUTE FUNCTION set_updated_at(); + +CREATE INDEX idx_posts_user_id ON posts (user_id); +CREATE INDEX idx_posts_created_at ON posts (created_at); +CREATE INDEX idx_posts_is_private ON posts (is_private); + +-- Full-text GIN index (replaces MySQL FULLTEXT) +CREATE INDEX idx_posts_search_tsv ON posts USING GIN (search_tsv); + +-- ---------- Post Categories (Many-to-Many) ---------- CREATE TABLE post_categories ( - post_id INT NOT NULL, - category_id INT NOT NULL, - PRIMARY KEY (post_id, category_id), + post_id BIGINT NOT NULL, + category_id BIGINT NOT NULL, + PRIMARY KEY (post_id, category_id), + CONSTRAINT fk_pc_post FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE, - INDEX idx_category_id (category_id) + CONSTRAINT fk_pc_category + FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE ); --- User Category Follows (for feed recommendations) +CREATE INDEX idx_post_categories_category_id ON post_categories (category_id); + +-- ---------- User Category Follows ---------- CREATE TABLE user_category_follows ( - user_id INT NOT NULL, - category_id INT NOT NULL, - followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, category_id), + user_id BIGINT NOT NULL, + category_id BIGINT NOT NULL, + followed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, category_id), + CONSTRAINT fk_ucf_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_category_id (category_id) + CONSTRAINT fk_ucf_category + FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE ); --- User Follows (following other users) +CREATE INDEX idx_ucf_user_id ON user_category_follows (user_id); +CREATE INDEX idx_ucf_category_id ON user_category_follows (category_id); + +-- ---------- User Follows ---------- CREATE TABLE user_follows ( - follower_id INT NOT NULL, - following_id INT NOT NULL, - followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (follower_id, following_id), + follower_id BIGINT NOT NULL, + following_id BIGINT NOT NULL, + followed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (follower_id, following_id), + CONSTRAINT fk_uf_follower FOREIGN KEY (follower_id) REFERENCES users(user_id) ON DELETE CASCADE, - FOREIGN KEY (following_id) REFERENCES users(user_id) ON DELETE CASCADE, - INDEX idx_follower_id (follower_id), - INDEX idx_following_id (following_id) + CONSTRAINT fk_uf_following + FOREIGN KEY (following_id) REFERENCES users(user_id) ON DELETE CASCADE ); --- Post Likes (engagement tracking) +CREATE INDEX idx_user_follows_follower_id ON user_follows (follower_id); +CREATE INDEX idx_user_follows_following_id ON user_follows (following_id); + +-- ---------- Post Likes ---------- CREATE TABLE post_likes ( - user_id INT NOT NULL, - post_id INT NOT NULL, - liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, post_id), + user_id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + liked_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, post_id), + CONSTRAINT fk_pl_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, - INDEX idx_post_id (post_id), - INDEX idx_liked_at (liked_at) + CONSTRAINT fk_pl_post + FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE ); --- Comments (engagement tracking) +CREATE INDEX idx_post_likes_post_id ON post_likes (post_id); +CREATE INDEX idx_post_likes_liked_at ON post_likes (liked_at); + +-- ---------- Comments ---------- CREATE TABLE comments ( - comment_id INT PRIMARY KEY AUTO_INCREMENT, - post_id INT NOT NULL, - user_id INT NOT NULL, - comment_text TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + comment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + post_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + comment_text TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT fk_comments_post FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - INDEX idx_post_id (post_id), - INDEX idx_user_id (user_id), - INDEX idx_created_at (created_at) + CONSTRAINT fk_comments_user + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ); --- Audio Listening History (for recommendations) +CREATE TRIGGER comments_set_updated_at +BEFORE UPDATE ON comments +FOR EACH ROW +EXECUTE FUNCTION set_updated_at(); + +CREATE INDEX idx_comments_post_id ON comments (post_id); +CREATE INDEX idx_comments_user_id ON comments (user_id); +CREATE INDEX idx_comments_created_at ON comments (created_at); + +-- ---------- Audio Listening History ---------- CREATE TABLE audio_listening_history ( - history_id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - post_id INT NOT NULL, - listened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - listen_duration_seconds INT, -- How long they actually listened - completed BOOLEAN DEFAULT FALSE, -- Did they listen to the end? + history_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + listened_at TIMESTAMPTZ NOT NULL DEFAULT now(), + listen_duration_seconds INT, + completed BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT fk_alh_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_post_id (post_id), - INDEX idx_listened_at (listened_at) + CONSTRAINT fk_alh_post + FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE ); --- Search History (for recommendations) +CREATE INDEX idx_alh_user_id ON audio_listening_history (user_id); +CREATE INDEX idx_alh_post_id ON audio_listening_history (post_id); +CREATE INDEX idx_alh_listened_at ON audio_listening_history (listened_at); + +-- ---------- Search History ---------- CREATE TABLE search_history ( - search_id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - search_query VARCHAR(255) NOT NULL, - searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_searched_at (searched_at), - INDEX idx_search_query (search_query) + search_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL, + search_query VARCHAR(255) NOT NULL, + searched_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT fk_search_user + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ); --- Bookmarks/Saved Posts +CREATE INDEX idx_search_user_id ON search_history (user_id); +CREATE INDEX idx_search_searched_at ON search_history (searched_at); +CREATE INDEX idx_search_query ON search_history (search_query); + +-- ---------- Bookmarks ---------- CREATE TABLE bookmarks ( - user_id INT NOT NULL, - post_id INT NOT NULL, - bookmarked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, post_id), + user_id BIGINT NOT NULL, + post_id BIGINT NOT NULL, + bookmarked_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, post_id), + CONSTRAINT fk_bookmarks_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, - FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, - INDEX idx_user_id (user_id), - INDEX idx_bookmarked_at (bookmarked_at) + CONSTRAINT fk_bookmarks_post + FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE ); +CREATE INDEX idx_bookmarks_user_id ON bookmarks (user_id); +CREATE INDEX idx_bookmarks_bookmarked_at ON bookmarks (bookmarked_at); + -- ============================================ -- SAMPLE SEED DATA -- ============================================ --- Insert sample categories INSERT INTO categories (name, description) VALUES ('Historical Events', 'Posts about significant historical events and eras'), ('Cultural Traditions', 'Cultural practices, traditions, and heritage'), @@ -162,64 +221,67 @@ INSERT INTO categories (name, description) VALUES ('Local History', 'Community and local historical accounts'); -- ============================================ --- USEFUL QUERIES FOR THE APPLICATION +-- USEFUL QUERIES (Postgres parameter style: $1, $2, ...) -- ============================================ --- Query 1: Get personalized feed for a user --- (Combines posts from followed users, followed categories, and engagement patterns) +-- Query 1: Personalized feed /* SELECT DISTINCT p.*, u.username, u.display_name FROM posts p -INNER JOIN users u ON p.user_id = u.user_id +JOIN users u ON p.user_id = u.user_id LEFT JOIN post_categories pc ON p.post_id = pc.post_id -LEFT JOIN user_category_follows ucf ON pc.category_id = ucf.category_id AND ucf.user_id = ? -LEFT JOIN user_follows uf ON p.user_id = uf.following_id AND uf.follower_id = ? +LEFT JOIN user_category_follows ucf ON pc.category_id = ucf.category_id AND ucf.user_id = $1 +LEFT JOIN user_follows uf ON p.user_id = uf.following_id AND uf.follower_id = $1 WHERE p.is_private = FALSE - AND (ucf.user_id IS NOT NULL OR uf.follower_id IS NOT NULL OR p.user_id = ?) + AND (ucf.user_id IS NOT NULL OR uf.follower_id IS NOT NULL OR p.user_id = $1) ORDER BY p.created_at DESC LIMIT 50; */ --- Query 2: Search posts by text (uses FULLTEXT index) +-- Query 2: Full-text search (replaces MySQL MATCH ... AGAINST) +-- Tip: websearch_to_tsquery supports Google-like syntax; plainto_tsquery is simpler. /* -SELECT p.*, u.username, MATCH(p.title, p.transcribed_text) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance +SELECT p.*, u.username, + ts_rank(p.search_tsv, websearch_to_tsquery('english', $1)) AS relevance FROM posts p -INNER JOIN users u ON p.user_id = u.user_id -WHERE MATCH(p.title, p.transcribed_text) AGAINST(? IN NATURAL LANGUAGE MODE) - AND (p.is_private = FALSE OR p.user_id = ?) +JOIN users u ON p.user_id = u.user_id +WHERE p.search_tsv @@ websearch_to_tsquery('english', $1) + AND (p.is_private = FALSE OR p.user_id = $2) ORDER BY relevance DESC LIMIT 50; */ --- Query 3: Get user's private posts +-- Query 3: User's private posts /* -SELECT p.*, COUNT(DISTINCT pl.user_id) as like_count, COUNT(DISTINCT c.comment_id) as comment_count +SELECT p.*, + COUNT(DISTINCT pl.user_id) AS like_count, + COUNT(DISTINCT c.comment_id) AS comment_count FROM posts p LEFT JOIN post_likes pl ON p.post_id = pl.post_id LEFT JOIN comments c ON p.post_id = c.post_id -WHERE p.user_id = ? AND p.is_private = TRUE +WHERE p.user_id = $1 AND p.is_private = TRUE GROUP BY p.post_id ORDER BY p.created_at DESC; */ --- Query 4: Get posts by category +-- Query 4: Posts by category /* SELECT p.*, u.username, u.display_name FROM posts p -INNER JOIN users u ON p.user_id = u.user_id -INNER JOIN post_categories pc ON p.post_id = pc.post_id -WHERE pc.category_id = ? AND p.is_private = FALSE +JOIN users u ON p.user_id = u.user_id +JOIN post_categories pc ON p.post_id = pc.post_id +WHERE pc.category_id = $1 AND p.is_private = FALSE ORDER BY p.created_at DESC LIMIT 50; */ --- Query 5: Get user's listening history +-- Query 5: Listening history /* SELECT p.*, u.username, alh.listened_at, alh.listen_duration_seconds, alh.completed FROM audio_listening_history alh -INNER JOIN posts p ON alh.post_id = p.post_id -INNER JOIN users u ON p.user_id = u.user_id -WHERE alh.user_id = ? +JOIN posts p ON alh.post_id = p.post_id +JOIN users u ON p.user_id = u.user_id +WHERE alh.user_id = $1 ORDER BY alh.listened_at DESC LIMIT 50; */