Update schema.sql

This commit is contained in:
Gaumit Kauts
2026-02-14 16:29:15 -07:00
parent 44b235ad21
commit 01ad42f6a1

View File

@@ -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 ( CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100), display_name VARCHAR(100),
profile_image_url VARCHAR(255), profile_image_url VARCHAR(255),
bio TEXT, bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
is_active BOOLEAN DEFAULT TRUE, is_active BOOLEAN NOT NULL DEFAULT TRUE
INDEX idx_username (username),
INDEX idx_email (email)
); );
-- 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 ( CREATE TABLE categories (
category_id INT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
description TEXT, description TEXT,
parent_category_id INT NULL, -- For subcategories parent_category_id BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (parent_category_id) REFERENCES categories(category_id) ON DELETE SET NULL, CONSTRAINT fk_parent_category
INDEX idx_name (name) 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 ( CREATE TABLE posts (
post_id INT PRIMARY KEY AUTO_INCREMENT, post_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT NOT NULL, user_id BIGINT NOT NULL,
title VARCHAR(255), title VARCHAR(255),
transcribed_text TEXT, -- Text generated from audio transcribed_text TEXT,
audio_url VARCHAR(255) NOT NULL, -- URL to stored audio file audio_url VARCHAR(255) NOT NULL,
audio_duration_seconds INT, -- Duration in seconds audio_duration_seconds INT,
image_url VARCHAR(255), -- Optional image image_url VARCHAR(255),
is_private BOOLEAN DEFAULT FALSE, -- TRUE = private, FALSE = public is_private BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_user_id (user_id), -- Postgres full-text search column (stored generated)
INDEX idx_created_at (created_at), search_tsv tsvector GENERATED ALWAYS AS (
INDEX idx_is_private (is_private), to_tsvector('english', coalesce(title,'') || ' ' || coalesce(transcribed_text,''))
FULLTEXT idx_fulltext_search (title, 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 ( CREATE TABLE post_categories (
post_id INT NOT NULL, post_id BIGINT NOT NULL,
category_id INT NOT NULL, category_id BIGINT NOT NULL,
PRIMARY KEY (post_id, category_id), PRIMARY KEY (post_id, category_id),
CONSTRAINT fk_pc_post
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE, CONSTRAINT fk_pc_category
INDEX idx_category_id (category_id) 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 ( CREATE TABLE user_category_follows (
user_id INT NOT NULL, user_id BIGINT NOT NULL,
category_id INT NOT NULL, category_id BIGINT NOT NULL,
followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, followed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, category_id), PRIMARY KEY (user_id, category_id),
CONSTRAINT fk_ucf_user
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE, CONSTRAINT fk_ucf_category
INDEX idx_user_id (user_id), FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE
INDEX idx_category_id (category_id)
); );
-- 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 ( CREATE TABLE user_follows (
follower_id INT NOT NULL, follower_id BIGINT NOT NULL,
following_id INT NOT NULL, following_id BIGINT NOT NULL,
followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, followed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (follower_id, following_id), PRIMARY KEY (follower_id, following_id),
CONSTRAINT fk_uf_follower
FOREIGN KEY (follower_id) REFERENCES users(user_id) ON DELETE CASCADE, FOREIGN KEY (follower_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (following_id) REFERENCES users(user_id) ON DELETE CASCADE, CONSTRAINT fk_uf_following
INDEX idx_follower_id (follower_id), FOREIGN KEY (following_id) REFERENCES users(user_id) ON DELETE CASCADE
INDEX idx_following_id (following_id)
); );
-- 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 ( CREATE TABLE post_likes (
user_id INT NOT NULL, user_id BIGINT NOT NULL,
post_id INT NOT NULL, post_id BIGINT NOT NULL,
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, liked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, post_id), PRIMARY KEY (user_id, post_id),
CONSTRAINT fk_pl_user
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, CONSTRAINT fk_pl_post
INDEX idx_post_id (post_id), FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
INDEX idx_liked_at (liked_at)
); );
-- 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 ( CREATE TABLE comments (
comment_id INT PRIMARY KEY AUTO_INCREMENT, comment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
post_id INT NOT NULL, post_id BIGINT NOT NULL,
user_id INT NOT NULL, user_id BIGINT NOT NULL,
comment_text TEXT NOT NULL, comment_text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fk_comments_post
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, CONSTRAINT fk_comments_user
INDEX idx_post_id (post_id), FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
); );
-- 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 ( CREATE TABLE audio_listening_history (
history_id INT PRIMARY KEY AUTO_INCREMENT, history_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT NOT NULL, user_id BIGINT NOT NULL,
post_id INT NOT NULL, post_id BIGINT NOT NULL,
listened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, listened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
listen_duration_seconds INT, -- How long they actually listened listen_duration_seconds INT,
completed BOOLEAN DEFAULT FALSE, -- Did they listen to the end? completed BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT fk_alh_user
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, CONSTRAINT fk_alh_post
INDEX idx_user_id (user_id), FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
INDEX idx_post_id (post_id),
INDEX idx_listened_at (listened_at)
); );
-- 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 ( CREATE TABLE search_history (
search_id INT PRIMARY KEY AUTO_INCREMENT, search_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT NOT NULL, user_id BIGINT NOT NULL,
search_query VARCHAR(255) NOT NULL, search_query VARCHAR(255) NOT NULL,
searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, searched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, CONSTRAINT fk_search_user
INDEX idx_user_id (user_id), FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
INDEX idx_searched_at (searched_at),
INDEX idx_search_query (search_query)
); );
-- 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 ( CREATE TABLE bookmarks (
user_id INT NOT NULL, user_id BIGINT NOT NULL,
post_id INT NOT NULL, post_id BIGINT NOT NULL,
bookmarked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, bookmarked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, post_id), PRIMARY KEY (user_id, post_id),
CONSTRAINT fk_bookmarks_user
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, CONSTRAINT fk_bookmarks_post
INDEX idx_user_id (user_id), FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
INDEX idx_bookmarked_at (bookmarked_at)
); );
CREATE INDEX idx_bookmarks_user_id ON bookmarks (user_id);
CREATE INDEX idx_bookmarks_bookmarked_at ON bookmarks (bookmarked_at);
-- ============================================ -- ============================================
-- SAMPLE SEED DATA -- SAMPLE SEED DATA
-- ============================================ -- ============================================
-- Insert sample categories
INSERT INTO categories (name, description) VALUES INSERT INTO categories (name, description) VALUES
('Historical Events', 'Posts about significant historical events and eras'), ('Historical Events', 'Posts about significant historical events and eras'),
('Cultural Traditions', 'Cultural practices, traditions, and heritage'), ('Cultural Traditions', 'Cultural practices, traditions, and heritage'),
@@ -162,64 +221,67 @@ INSERT INTO categories (name, description) VALUES
('Local History', 'Community and local historical accounts'); ('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 -- Query 1: Personalized feed
-- (Combines posts from followed users, followed categories, and engagement patterns)
/* /*
SELECT DISTINCT p.*, u.username, u.display_name SELECT DISTINCT p.*, u.username, u.display_name
FROM posts p 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 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_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 = ? LEFT JOIN user_follows uf ON p.user_id = uf.following_id AND uf.follower_id = $1
WHERE p.is_private = FALSE 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 ORDER BY p.created_at DESC
LIMIT 50; 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 FROM posts p
INNER JOIN users u ON p.user_id = u.user_id JOIN users u ON p.user_id = u.user_id
WHERE MATCH(p.title, p.transcribed_text) AGAINST(? IN NATURAL LANGUAGE MODE) WHERE p.search_tsv @@ websearch_to_tsquery('english', $1)
AND (p.is_private = FALSE OR p.user_id = ?) AND (p.is_private = FALSE OR p.user_id = $2)
ORDER BY relevance DESC ORDER BY relevance DESC
LIMIT 50; 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 FROM posts p
LEFT JOIN post_likes pl ON p.post_id = pl.post_id LEFT JOIN post_likes pl ON p.post_id = pl.post_id
LEFT JOIN comments c ON p.post_id = c.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 GROUP BY p.post_id
ORDER BY p.created_at DESC; ORDER BY p.created_at DESC;
*/ */
-- Query 4: Get posts by category -- Query 4: Posts by category
/* /*
SELECT p.*, u.username, u.display_name SELECT p.*, u.username, u.display_name
FROM posts p FROM posts p
INNER JOIN users u ON p.user_id = u.user_id JOIN users u ON p.user_id = u.user_id
INNER JOIN post_categories pc ON p.post_id = pc.post_id JOIN post_categories pc ON p.post_id = pc.post_id
WHERE pc.category_id = ? AND p.is_private = FALSE WHERE pc.category_id = $1 AND p.is_private = FALSE
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT 50; 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 SELECT p.*, u.username, alh.listened_at, alh.listen_duration_seconds, alh.completed
FROM audio_listening_history alh FROM audio_listening_history alh
INNER JOIN posts p ON alh.post_id = p.post_id JOIN posts p ON alh.post_id = p.post_id
INNER JOIN users u ON p.user_id = u.user_id JOIN users u ON p.user_id = u.user_id
WHERE alh.user_id = ? WHERE alh.user_id = $1
ORDER BY alh.listened_at DESC ORDER BY alh.listened_at DESC
LIMIT 50; LIMIT 50;
*/ */