diff --git a/.aiignore b/.aiignore new file mode 100644 index 0000000..a7db0d7 --- /dev/null +++ b/.aiignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +build/ \ No newline at end of file diff --git a/README.md b/README.md index ec6afda..93d2f58 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,97 @@ Add these credentials to your .env file at the project root: plaintext GITHUB_CLIENT_ID="your-github-client-id" GITHUB_CLIENT_SECRET="your-github-client-secret" + +## connect to postgres on projectium.com + +psql -h localhost -U flyer_crawler_user -d "flyer-crawler-prod" -W + + +## postgis + +flyer-crawler-prod=> SELECT version(); + version +------------------------------------------------------------------------------------------------------------------------------------------ + PostgreSQL 14.19 (Ubuntu 14.19-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0, 64-bit +(1 row) + +flyer-crawler-prod=> SELECT PostGIS_Full_Version(); + postgis_full_version +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- + POSTGIS="3.2.0 c3e3cc0" [EXTENSION] PGSQL="140" GEOS="3.10.2-CAPI-1.16.0" PROJ="8.2.1" LIBXML="2.9.12" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)" +(1 row) + + +## production postgres setup + +Part 1: Production Database Setup +This database will be the live, persistent storage for your application. + +Step 1: Install PostgreSQL (if not already installed) +First, ensure PostgreSQL is installed on your server. + +bash +sudo apt update +sudo apt install postgresql postgresql-contrib +Step 2: Create the Production Database and User +It's best practice to create a dedicated, non-superuser role for your application to connect with. + +Switch to the postgres system user to get superuser access to the database. + +bash +sudo -u postgres psql +Inside the psql shell, run the following SQL commands. Remember to replace 'a_very_strong_password' with a secure password that you will manage with a secrets tool or in your .env file. + +sql +-- Create a new role (user) for your application +CREATE ROLE flyer_crawler_user WITH LOGIN PASSWORD 'a_very_strong_password'; + +-- Create the production database and assign ownership to the new user +CREATE DATABASE "flyer-crawler-prod" WITH OWNER = flyer_crawler_user; + +-- Exit the psql shell + +Step 3: Apply the Master Schema +Now, you'll populate your new database with all the tables, functions, and initial data. Your master_schema_rollup.sql file is perfect for this. + +Navigate to your project's root directory on the server. + +Run the following command to execute the master schema script against your new production database. You will be prompted for the password you created in the previous step. + +bash +psql -U flyer_crawler_user -d "flyer-crawler-prod" -f sql/master_schema_rollup.sql +This single command creates all tables, extensions (pg_trgm, postgis), functions, and triggers, and seeds essential data like categories and master items. + +Step 4: Seed the Admin Account +Your application has a separate script to create the initial admin user. + +Ensure your .env file on the server is configured with the correct production database credentials (DB_USER, DB_PASSWORD, DB_DATABASE="flyer-crawler-prod", etc.). + +Run the admin seeding script using tsx. + +bash +npx tsx src/db/seed_admin_account.ts +Your production database is now ready! Your application can connect to it using the flyer_crawler_user role and the credentials in your .env file. + +Part 2: Test Database Setup (for CI/CD) +Your Gitea workflow (deploy.yml) already automates the creation and teardown of the test database during the pipeline run. The steps below are for understanding what the workflow does and for manual setup if you ever need to run tests outside the CI pipeline. + +The process your CI pipeline follows is: + +Setup (sql/test_setup.sql): + +As the postgres superuser, it runs sql/test_setup.sql. +This creates a temporary role named test_runner. +It creates a separate database named "flyer-crawler-test" owned by test_runner. +Schema Application (src/tests/setup/global-setup.ts): + +The test runner (vitest) executes the global-setup.ts file. +This script connects to the "flyer-crawler-test" database using the temporary credentials. +It then runs the same sql/master_schema_rollup.sql file, ensuring your test database has the exact same structure as production. +Test Execution: + +Your tests run against this clean, isolated "flyer-crawler-test" database. +Teardown (sql/test_teardown.sql): + +After tests complete (whether they pass or fail), the if: always() step in your workflow ensures that sql/test_teardown.sql is executed. +This script terminates any lingering connections to the test database, drops the "flyer-crawler-test" database completely, and drops the test_runner role. \ No newline at end of file diff --git a/sql/Initial_triggers_and_functions.sql b/sql/Initial_triggers_and_functions.sql index 419f1dd..7fe9d5f 100644 --- a/sql/Initial_triggers_and_functions.sql +++ b/sql/Initial_triggers_and_functions.sql @@ -22,9 +22,13 @@ BEGIN VALUES (new_profile_id, 'Main Shopping List'); -- Log the new user event - INSERT INTO public.user_activity_log (user_id, activity_type, entity_id, details) - VALUES (new.id, 'new_user', new.id, jsonb_build_object('full_name', user_meta_data->>'full_name')); - + INSERT INTO public.activity_log (user_id, action, display_text, icon, details) + VALUES (new.id, 'user_registered', + COALESCE(user_meta_data->>'full_name', new.email) || ' has registered.', + 'user-plus', + jsonb_build_object('email', new.email) + ); + RETURN new; END; $$ LANGUAGE plpgsql; @@ -78,6 +82,21 @@ CREATE TRIGGER on_notifications_updated BEFORE UPDATE ON public.notifications FO DROP TRIGGER IF EXISTS on_item_price_history_updated ON public.item_price_history; CREATE TRIGGER on_item_price_history_updated BEFORE UPDATE ON public.item_price_history FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); +DROP TRIGGER IF EXISTS on_menu_plans_updated ON public.menu_plans; +CREATE TRIGGER on_menu_plans_updated BEFORE UPDATE ON public.menu_plans FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_shared_shopping_lists_updated ON public.shared_shopping_lists; +CREATE TRIGGER on_shared_shopping_lists_updated BEFORE UPDATE ON public.shared_shopping_lists FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_shared_menu_plans_updated ON public.shared_menu_plans; +CREATE TRIGGER on_shared_menu_plans_updated BEFORE UPDATE ON public.shared_menu_plans FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_favorite_recipes_updated ON public.favorite_recipes; +CREATE TRIGGER on_favorite_recipes_updated BEFORE UPDATE ON public.favorite_recipes FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_user_follows_updated ON public.user_follows; +CREATE TRIGGER on_user_follows_updated BEFORE UPDATE ON public.user_follows FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + -- (Apply to other tables as needed...) -- Apply the trigger to the 'pantry_items' table. @@ -246,11 +265,12 @@ CREATE TRIGGER on_recipe_rating_change CREATE OR REPLACE FUNCTION public.log_new_recipe() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO public.user_activity_log (user_id, activity_type, entity_id, details) + INSERT INTO public.activity_log (user_id, action, display_text, icon, details) VALUES ( NEW.user_id, - 'new_recipe', - NEW.id::text, + 'recipe_created', + (SELECT full_name FROM public.profiles WHERE id = NEW.user_id) || ' created a new recipe: ' || NEW.name, + 'chef-hat', jsonb_build_object('recipe_name', NEW.name) ); RETURN NEW; @@ -261,20 +281,23 @@ $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS on_new_recipe_created ON public.recipes; CREATE TRIGGER on_new_recipe_created AFTER INSERT ON public.recipes - FOR EACH ROW EXECUTE FUNCTION public.log_new_recipe(); + FOR EACH ROW + WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes. + EXECUTE FUNCTION public.log_new_recipe(); -- 7. Trigger function to log the creation of a new flyer. CREATE OR REPLACE FUNCTION public.log_new_flyer() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO public.user_activity_log (activity_type, entity_id, details) + INSERT INTO public.activity_log (action, display_text, icon, details) VALUES ( - 'new_flyer', - NEW.id::text, + 'flyer_uploaded', + 'A new flyer for ' || (SELECT name FROM public.stores WHERE id = NEW.store_id) || ' has been uploaded.', + 'file-text', jsonb_build_object( 'store_name', (SELECT name FROM public.stores WHERE id = NEW.store_id), - 'valid_from', NEW.valid_from, - 'valid_to', NEW.valid_to + 'valid_from', to_char(NEW.valid_from, 'YYYY-MM-DD'), + 'valid_to', to_char(NEW.valid_to, 'YYYY-MM-DD') ) ); RETURN NEW; @@ -291,13 +314,14 @@ CREATE TRIGGER on_new_flyer_created CREATE OR REPLACE FUNCTION public.log_new_favorite_recipe() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO public.user_activity_log (user_id, activity_type, entity_id, details) + INSERT INTO public.activity_log (user_id, action, display_text, icon, details) VALUES ( NEW.user_id, - 'favorite_recipe', - NEW.recipe_id::text, + 'recipe_favorited', + (SELECT full_name FROM public.profiles WHERE id = NEW.user_id) || ' favorited the recipe: ' || (SELECT name FROM public.recipes WHERE id = NEW.recipe_id), + 'heart', jsonb_build_object( - 'recipe_name', (SELECT name FROM public.recipes WHERE id = NEW.recipe_id) + 'recipe_id', NEW.recipe_id ) ); RETURN NEW; @@ -314,14 +338,15 @@ CREATE TRIGGER on_new_favorite_recipe CREATE OR REPLACE FUNCTION public.log_new_list_share() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO public.user_activity_log (user_id, activity_type, entity_id, details) + INSERT INTO public.activity_log (user_id, action, display_text, icon, details) VALUES ( NEW.shared_by_user_id, - 'share_shopping_list', - NEW.shopping_list_id::text, + 'list_shared', + (SELECT full_name FROM public.profiles WHERE id = NEW.shared_by_user_id) || ' shared a shopping list.', + 'share-2', jsonb_build_object( 'list_name', (SELECT name FROM public.shopping_lists WHERE id = NEW.shopping_list_id), - 'shared_with_name', (SELECT full_name FROM public.profiles WHERE id = NEW.shared_with_user_id) + 'shared_with_user_id', NEW.shared_with_user_id ) ); RETURN NEW; @@ -334,6 +359,9 @@ CREATE TRIGGER on_new_list_share AFTER INSERT ON public.shared_shopping_lists FOR EACH ROW EXECUTE FUNCTION public.log_new_list_share(); +-- ============================================================================ +-- PART 6: DATABASE FUNCTIONS +-- ============================================================================ -- Function to find the best current sale price for a user's watched items. -- This function queries all currently active flyers to find the lowest price -- for each item on a specific user's watchlist. @@ -966,8 +994,9 @@ CREATE OR REPLACE FUNCTION public.get_activity_log(p_limit INTEGER DEFAULT 20, p RETURNS TABLE ( id BIGINT, user_id UUID, - activity_type TEXT, - entity_id TEXT, + action TEXT, + display_text TEXT, + icon TEXT, details JSONB, created_at TIMESTAMPTZ, user_full_name TEXT, @@ -978,15 +1007,9 @@ STABLE SECURITY INVOKER AS $$ SELECT - al.id, - al.user_id, - al.activity_type, - al.entity_id, - al.details, - al.created_at, - p.full_name AS user_full_name, - p.avatar_url AS user_avatar_url - FROM public.user_activity_log al + al.id, al.user_id, al.action, al.display_text, al.icon, al.details, al.created_at, + p.full_name, p.avatar_url + FROM public.activity_log al -- Join with profiles to get user details for display. -- LEFT JOIN is used because some activities might be system-generated (user_id is NULL). LEFT JOIN public.profiles p ON al.user_id = p.id @@ -994,24 +1017,17 @@ AS $$ al.created_at DESC LIMIT p_limit OFFSET p_offset; -$$;) +$$; -- Function to get a user's profile by their ID, combining data from users and profiles tables. CREATE OR REPLACE FUNCTION public.get_user_profile_by_id(p_user_id UUID) RETURNS TABLE ( id UUID, email TEXT, - full_name TEX, - id UUIDT, - email TEXT, full_name TEXT, avatar_url TEXT, - preferenaes JSONB, - role TEXT, - cvatar_url TEXT, preferences JSONB, - role TEXT,T - updated_at TIMESTAMPZ + role TEXT, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ ) @@ -1027,20 +1043,9 @@ AS $$ p.preferences, p.role, p.created_at, - p.updated_aet; - u.id, - u.tmail, - p.full_name, - p.avaar_url, - p.preferences, - p.role, - p.created_at, p.updated_at FROM public.users u JOIN public.profiles p ON u.id = p.id - WHERE u.id = p_user_id - FROM public.users u - JOIN public.profiles p ON u.id = p.id WHERE u.id = p_user_id; $$; @@ -1081,8 +1086,9 @@ CREATE OR REPLACE FUNCTION public.get_user_feed(p_user_id UUID, p_limit INTEGER RETURNS TABLE ( id BIGINT, user_id UUID, - activity_type TEXT, - entity_id TEXT, + action TEXT, + display_text TEXT, + icon TEXT, details JSONB, created_at TIMESTAMPTZ, user_full_name TEXT, @@ -1098,23 +1104,17 @@ AS $$ ) -- Final Selection: Get activities from the log where the user_id is in the followed list. SELECT - al.id, - al.user_id, - al.activity_type, - al.entity_id, - al.details, - al.created_at, - p.full_name AS user_full_name, - p.avatar_url AS user_avatar_url - FROM public.user_activity_log al + al.id, al.user_id, al.action, al.display_text, al.icon, al.details, al.created_at, + p.full_name, p.avatar_url + FROM public.activity_log al JOIN public.profiles p ON al.user_id = p.id WHERE al.user_id IN (SELECT following_id FROM FollowedUsers) - -- We can filter for specific activity types to make the feed more relevant. - AND al.activity_type IN ( - 'new_recipe', - 'favorite_recipe', - 'share_shopping_list' + -- We can filter for specific action types to make the feed more relevant. + AND al.action IN ( + 'recipe_created', + 'recipe_favorited', + 'list_shared' -- 'new_recipe_rating' could be added here later ) ORDER BY diff --git a/sql/drop_tables.sql b/sql/drop_tables.sql index 137e301..9a15a21 100644 --- a/sql/drop_tables.sql +++ b/sql/drop_tables.sql @@ -8,7 +8,8 @@ DROP TABLE IF EXISTS public.user_follows CASCADE; DROP TABLE IF EXISTS public.user_appliances CASCADE; DROP TABLE IF EXISTS public.recipe_collections CASCADE; -DROP TABLE IF EXISTS public.user_activity_log CASCADE; +DROP TABLE IF EXISTS public.recipe_collection_items CASCADE; +DROP TABLE IF EXISTS public.activity_log CASCADE; DROP TABLE IF EXISTS public.favorite_stores CASCADE; DROP TABLE IF EXISTS public.favorite_recipes CASCADE; DROP TABLE IF EXISTS public.user_item_aliases CASCADE; @@ -23,7 +24,6 @@ DROP TABLE IF EXISTS public.menu_plans CASCADE; DROP TABLE IF EXISTS public.recipe_ratings CASCADE; DROP TABLE IF EXISTS public.recipe_tags CASCADE; DROP TABLE IF EXISTS public.tags CASCADE; -DROP TABLE IF EXISTS public.recipe_collection_items CASCADE; DROP TABLE IF EXISTS public.shared_shopping_lists CASCADE; DROP TABLE IF EXISTS public.recipe_ingredients CASCADE; DROP TABLE IF EXISTS public.recipes CASCADE; @@ -57,4 +57,4 @@ DROP TABLE IF EXISTS public.categories CASCADE; DROP TABLE IF EXISTS public.profiles CASCADE; DROP TABLE IF EXISTS public.password_reset_tokens CASCADE; DROP TABLE IF EXISTS public.users CASCADE; -DROP TABLE IF EXISTS public.unmatched_flyer_items CASCADE; \ No newline at end of file +DROP TABLE IF EXISTS public.unmatched_flyer_items CASCADE; diff --git a/sql/generate_rollup.ps1 b/sql/generate_rollup.ps1 new file mode 100644 index 0000000..096fafb --- /dev/null +++ b/sql/generate_rollup.ps1 @@ -0,0 +1,32 @@ +<# +.SYNOPSIS + SQL ROLLUP GENERATION SCRIPT (POWERSHELL) +.DESCRIPTION + This script automatically generates the 'master_schema_rollup.sql' file by + concatenating the individual SQL component files in the correct order. + + WARNING: This will overwrite the existing master_schema_rollup.sql file. +.EXAMPLE + From the root of the project, run: + .\sql\generate_rollup.ps1 +#> + +# Set the script to stop on errors +$ErrorActionPreference = "Stop" + +# Define file paths relative to the script's location +$PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition +$ProjectRoot = Resolve-Path -Path (Join-Path $PSScriptRoot "..") +$MasterFile = Join-Path $ProjectRoot "sql\master_schema_rollup.sql" + +# The individual files to concatenate, IN ORDER. +$SourceFiles = @( + (Join-Path $ProjectRoot "sql\initial_schema.sql"), + (Join-Path $ProjectRoot "sql\initial_data.sql"), + (Join-Path $ProjectRoot "sql\initial_triggers_and_functions.sql") +) + +Write-Host "Generating '$MasterFile' from source files..." +Get-Content -Path $SourceFiles | Set-Content -Path $MasterFile -Encoding UTF8 +Write-Host "✅ Success: '$MasterFile' has been generated." -ForegroundColor Green + diff --git a/sql/generate_rollup.sh b/sql/generate_rollup.sh new file mode 100644 index 0000000..692f13d --- /dev/null +++ b/sql/generate_rollup.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# ============================================================================ +# SQL ROLLUP GENERATION SCRIPT (BASH) +# ============================================================================ +# Purpose: +# This script automatically generates the 'master_schema_rollup.sql' file by +# concatenating the individual SQL component files in the correct order. +# +# WARNING: This will overwrite the existing master_schema_rollup.sql file. +# +# Usage: +# From the root of the project, run: +# bash sql/generate_rollup.sh +# ============================================================================ + +# Set the script to exit immediately if a command fails +set -e + +# Define file paths relative to the project root +SQL_DIR="sql" +MASTER_FILE="$SQL_DIR/master_schema_rollup.sql" + +# The individual files to concatenate, IN ORDER. +SOURCE_FILES=( + "$SQL_DIR/initial_schema.sql" + "$SQL_DIR/initial_data.sql" + "$SQL_DIR/initial_triggers_and_functions.sql" +) + +echo "Generating '$MASTER_FILE' from source files..." +cat "${SOURCE_FILES[@]}" > "$MASTER_FILE" +echo "✅ Success: '$MASTER_FILE' has been generated." \ No newline at end of file diff --git a/sql/initial_data.sql b/sql/initial_data.sql index 59b7ac0..584d8c9 100644 --- a/sql/initial_data.sql +++ b/sql/initial_data.sql @@ -165,7 +165,7 @@ BEGIN ('Simple Chicken and Rice', 'A quick and healthy weeknight meal with chicken, rice, and broccoli.', '1. Cook rice according to package directions. 2. Steam broccoli. 3. Pan-sear chicken breast until cooked through. 4. Combine and serve.', 10, 20, 4), ('Classic Spaghetti Bolognese', 'A rich and hearty meat sauce served over spaghetti, perfect for the whole family.', '1. Brown ground beef with onions and garlic. 2. Add tomatoes and simmer for 30 minutes. 3. Cook pasta. 4. Serve sauce over pasta.', 15, 45, 6), ('Vegetable Stir-fry', 'A fast, flavorful, and vegetarian stir-fry loaded with fresh vegetables.', '1. Chop all vegetables. 2. Heat oil in a wok or large pan. 3. Stir-fry vegetables for 5-7 minutes until tender-crisp. 4. Add soy sauce and serve immediately.', 10, 10, 3) - ON CONFLICT (name) DO NOTHING; + ON CONFLICT (name) WHERE user_id IS NULL DO NOTHING; SELECT id INTO chicken_recipe_id FROM public.recipes WHERE name = 'Simple Chicken and Rice'; SELECT id INTO bolognese_recipe_id FROM public.recipes WHERE name = 'Classic Spaghetti Bolognese'; diff --git a/sql/initial_schema.sql b/sql/initial_schema.sql index 2e01429..0873a07 100644 --- a/sql/initial_schema.sql +++ b/sql/initial_schema.sql @@ -1,16 +1,21 @@ --- EXTENSIONS FIRST? --- Enable trigram support for fuzzy string matching -CREATE EXTENSION IF NOT EXISTS pg_trgm; - --- First, enable the PostGIS extension if you haven't already. +-- ============================================================================ +-- PART 1: EXTENSIONS +-- ============================================================================ +-- Enable necessary PostgreSQL extensions. +-- postgis: For storing and querying geographic data (store locations). +-- pg_trgm: For trigram-based fuzzy string matching (improving item searches). CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- For generating UUIDs - --- 1. for public user profiles. +-- ============================================================================ +-- PART 2: TABLES +-- ============================================================================ +-- 1. Users - This replaces the Supabase `auth.users` table. CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, + password_hash TEXT, refresh_token TEXT, failed_login_attempts INTEGER DEFAULT 0, last_failed_login TIMESTAMPTZ, @@ -24,7 +29,23 @@ COMMENT ON COLUMN public.users.last_failed_login IS 'Timestamp of the last faile -- Add an index on the refresh_token for faster lookups when refreshing tokens. CREATE INDEX IF NOT EXISTS idx_users_refresh_token ON public.users(refresh_token); --- 2. for public user profiles. +-- 2. Log key user activities for analytics. +-- This needs to be created early as many triggers will insert into it. +CREATE TABLE IF NOT EXISTS public.activity_log ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + action TEXT NOT NULL, + display_text TEXT NOT NULL, + icon TEXT, + details JSONB, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.activity_log IS 'Logs key user and system actions for auditing and display in an activity feed.'; +CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log(user_id); + +-- 3. for public user profiles. +-- This table is linked to the users table and stores non-sensitive user data. CREATE TABLE IF NOT EXISTS public.profiles ( id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE, full_name TEXT, @@ -38,7 +59,7 @@ CREATE TABLE IF NOT EXISTS public.profiles ( ); COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.'; --- 3. The 'stores' table for normalized store data. +-- 4. The 'stores' table for normalized store data. CREATE TABLE IF NOT EXISTS public.stores ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -49,7 +70,7 @@ CREATE TABLE IF NOT EXISTS public.stores ( ); COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).'; --- 4. The 'categories' table for normalized category data. +-- 5. The 'categories' table for normalized category data. CREATE TABLE IF NOT EXISTS public.categories ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -58,7 +79,7 @@ CREATE TABLE IF NOT EXISTS public.categories ( ); COMMENT ON TABLE public.categories IS 'Stores a predefined list of grocery item categories (e.g., ''Fruits & Vegetables'', ''Dairy & Eggs'').'; --- 5. flyers' table +-- 6. flyers' table CREATE TABLE IF NOT EXISTS public.flyers ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, file_name TEXT NOT NULL, @@ -73,8 +94,15 @@ CREATE TABLE IF NOT EXISTS public.flyers ( ); COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.'; CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id); +COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").'; +COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored in Supabase Storage.'; +COMMENT ON COLUMN public.flyers.checksum IS 'A SHA-256 hash of the original file content to prevent duplicate processing.'; +COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a specific store in the `stores` table.'; +COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.'; +COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.'; +COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.'; --- 6. The 'master_grocery_items' table. This is the master dictionary. +-- 7. The 'master_grocery_items' table. This is the master dictionary. CREATE TABLE IF NOT EXISTS public.master_grocery_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -88,7 +116,7 @@ CREATE TABLE IF NOT EXISTS public.master_grocery_items ( COMMENT ON TABLE public.master_grocery_items IS 'The master dictionary of canonical grocery items. Each item has a unique name and is linked to a category.'; CREATE INDEX IF NOT EXISTS idx_master_grocery_items_category_id ON public.master_grocery_items(category_id); --- 7. The 'user_watched_items' table. This links to the master list. +-- 8. The 'user_watched_items' table. This links to the master list. CREATE TABLE IF NOT EXISTS public.user_watched_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -100,7 +128,7 @@ CREATE TABLE IF NOT EXISTS public.user_watched_items ( COMMENT ON TABLE public.user_watched_items IS 'A linking table that represents a user''s personal watchlist of grocery items.'; CREATE INDEX IF NOT EXISTS idx_user_watched_items_master_item_id ON public.user_watched_items(master_item_id); --- 8. The 'flyer_items' table. This stores individual items from flyers. +-- 9. The 'flyer_items' table. This stores individual items from flyers. CREATE TABLE IF NOT EXISTS public.flyer_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, flyer_id BIGINT REFERENCES public.flyers(id) ON DELETE CASCADE, @@ -119,14 +147,26 @@ CREATE TABLE IF NOT EXISTS public.flyer_items ( created_at TIMESTAMPTZ DEFAULT now() NOT NULL ); COMMENT ON TABLE public.flyer_items IS 'Stores individual items extracted from a specific flyer.'; -COMMENT ON COLUMN public.flyer_items.category_name IS 'Denormalized for easier display'; -COMMENT ON COLUMN public.flyer_items.product_id IS 'Future use for specific product linking'; +COMMENT ON COLUMN public.flyer_items.flyer_id IS 'Foreign key linking this item to its parent flyer in the `flyers` table.'; +COMMENT ON COLUMN public.flyer_items.item IS 'The raw item name as it appears in the flyer (e.g., "Granny Smith Apples").'; +COMMENT ON COLUMN public.flyer_items.price_display IS 'The raw price string from the flyer (e.g., "$3.99", "2 for $5.00").'; +COMMENT ON COLUMN public.flyer_items.price_in_cents IS 'The normalized price for a single item, in cents, for easier sorting and comparison.'; +COMMENT ON COLUMN public.flyer_items.quantity IS 'The raw quantity or deal description string (e.g., "per lb", "500g bag").'; +COMMENT ON COLUMN public.flyer_items.quantity_num IS 'The parsed primary numeric value from the quantity string (e.g., 500 from "500g").'; +COMMENT ON COLUMN public.flyer_items.master_item_id IS 'Foreign key linking this flyer item to its canonical entry in `master_grocery_items`. Null if no match was found.'; +COMMENT ON COLUMN public.flyer_items.category_id IS 'Foreign key to the item''s category. Can be redundant if master_item_id is set, but useful.'; +COMMENT ON COLUMN public.flyer_items.category_name IS 'The denormalized name of the category for faster UI display without extra joins.'; +COMMENT ON COLUMN public.flyer_items.unit_price IS 'A JSONB object storing the calculated unit price (e.g., {"value": 1.99, "unit": "lb"}) for standardized price comparisons.'; +COMMENT ON COLUMN public.flyer_items.product_id IS 'A foreign key for future use, to link to a specific product with a UPC code in the `products` table.'; CREATE INDEX IF NOT EXISTS idx_flyer_items_flyer_id ON public.flyer_items(flyer_id); CREATE INDEX IF NOT EXISTS idx_flyer_items_master_item_id ON public.flyer_items(master_item_id); CREATE INDEX IF NOT EXISTS idx_flyer_items_category_id ON public.flyer_items(category_id); CREATE INDEX IF NOT EXISTS idx_flyer_items_product_id ON public.flyer_items(product_id); +-- Add a GIN index to the 'item' column for fast fuzzy text searching. +-- This requires the pg_trgm extension. +CREATE INDEX IF NOT EXISTS flyer_items_item_trgm_idx ON public.flyer_items USING GIN (item gin_trgm_ops); --- 9. For user-defined alerts on watched items. +-- 10. For user-defined alerts on watched items. CREATE TABLE IF NOT EXISTS public.user_alerts ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_watched_item_id BIGINT NOT NULL REFERENCES public.user_watched_items(id) ON DELETE CASCADE, @@ -141,7 +181,7 @@ COMMENT ON COLUMN public.user_alerts.alert_type IS 'The condition that triggers COMMENT ON COLUMN public.user_alerts.threshold_value IS 'The numeric threshold for the alert condition (e.g., price in cents, or percentage).'; CREATE INDEX IF NOT EXISTS idx_user_alerts_user_watched_item_id ON public.user_alerts(user_watched_item_id); --- 10. Store notifications for users. +-- 11. Store notifications for users. CREATE TABLE IF NOT EXISTS public.notifications ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -156,7 +196,26 @@ COMMENT ON COLUMN public.notifications.content IS 'The notification message disp COMMENT ON COLUMN public.notifications.link_url IS 'A URL to navigate to when the notification is clicked.'; CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON public.notifications(user_id); --- 11. For aggregated, historical price data for master items. +-- 12. Store individual store locations with geographic data. +CREATE TABLE IF NOT EXISTS public.store_locations ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, + address TEXT NOT NULL, + city TEXT, + province_state TEXT, + postal_code TEXT, + location GEOGRAPHY(Point, 4326), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.store_locations IS 'Stores physical locations of stores with geographic data for proximity searches.'; +COMMENT ON COLUMN public.store_locations.location IS 'Geographic coordinates (longitude, latitude) of the store.'; +CREATE INDEX IF NOT EXISTS idx_store_locations_store_id ON public.store_locations(store_id); +-- Add a GIST index for efficient geographic queries. +-- This requires the postgis extension. +CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location); + +-- 13. For aggregated, historical price data for master items. CREATE TABLE IF NOT EXISTS public.item_price_history ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -179,7 +238,7 @@ COMMENT ON COLUMN public.item_price_history.data_points_count IS 'How many data CREATE INDEX IF NOT EXISTS idx_item_price_history_master_item_id ON public.item_price_history(master_item_id); CREATE INDEX IF NOT EXISTS idx_item_price_history_store_location_id ON public.item_price_history(store_location_id); --- 12. Map various names to a single master grocery item. +-- 14. Map various names to a single master grocery item. CREATE TABLE IF NOT EXISTS public.master_item_aliases ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -191,7 +250,7 @@ COMMENT ON TABLE public.master_item_aliases IS 'Stores synonyms or alternative n COMMENT ON COLUMN public.master_item_aliases.alias IS 'An alternative name, e.g., "Ground Chuck" for the master item "Ground Beef".'; CREATE INDEX IF NOT EXISTS idx_master_item_aliases_master_item_id ON public.master_item_aliases(master_item_id); --- 13. For user shopping lists. +-- 15. For user shopping lists. CREATE TABLE IF NOT EXISTS public.shopping_lists ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -202,7 +261,7 @@ CREATE TABLE IF NOT EXISTS public.shopping_lists ( COMMENT ON TABLE public.shopping_lists IS 'Stores user-created shopping lists, e.g., "Weekly Groceries".'; CREATE INDEX IF NOT EXISTS idx_shopping_lists_user_id ON public.shopping_lists(user_id); --- 13. For user shopping lists. +-- 16. For items in a user's shopping list. CREATE TABLE IF NOT EXISTS public.shopping_list_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, @@ -221,7 +280,7 @@ COMMENT ON COLUMN public.shopping_list_items.is_purchased IS 'Lets users check i CREATE INDEX IF NOT EXISTS idx_shopping_list_items_shopping_list_id ON public.shopping_list_items(shopping_list_id); CREATE INDEX IF NOT EXISTS idx_shopping_list_items_master_item_id ON public.shopping_list_items(master_item_id); --- 14. Manage shared access to shopping lists. +-- 17. Manage shared access to shopping lists. CREATE TABLE IF NOT EXISTS public.shared_shopping_lists ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, @@ -237,7 +296,21 @@ CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shopping_list_id ON public. CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_by_user_id ON public.shared_shopping_lists(shared_by_user_id); CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_with_user_id ON public.shared_shopping_lists(shared_with_user_id); --- 15. Manage shared access to menu plans. +-- 18. Store a user's collection of planned meals for a date range. +CREATE TABLE IF NOT EXISTS public.menu_plans ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT date_range_check CHECK (end_date >= start_date) +); +COMMENT ON TABLE public.menu_plans IS 'Represents a user''s meal plan for a specific period, e.g., "Week of Oct 23".'; +CREATE INDEX IF NOT EXISTS idx_menu_plans_user_id ON public.menu_plans(user_id); + +-- 19. Manage shared access to menu plans. CREATE TABLE IF NOT EXISTS public.shared_menu_plans ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, @@ -253,7 +326,7 @@ CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_menu_plan_id ON public.shared_m CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_by_user_id ON public.shared_menu_plans(shared_by_user_id); CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_with_user_id ON public.shared_menu_plans(shared_with_user_id); --- 16. Store user-submitted corrections for flyer items. +-- 20. Store user-submitted corrections for flyer items. CREATE TABLE IF NOT EXISTS public.suggested_corrections ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, @@ -273,7 +346,7 @@ COMMENT ON COLUMN public.suggested_corrections.status IS 'The moderation status CREATE INDEX IF NOT EXISTS idx_suggested_corrections_flyer_item_id ON public.suggested_corrections(flyer_item_id); CREATE INDEX IF NOT EXISTS idx_suggested_corrections_user_id ON public.suggested_corrections(user_id); --- 17. For prices submitted directly by users from in-store. +-- 21. For prices submitted directly by users from in-store. CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id), @@ -292,7 +365,7 @@ COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_user_id ON public.user_submitted_prices(user_id); CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_master_item_id ON public.user_submitted_prices(master_item_id); --- 18. Log flyer items that could not be automatically matched to a master item. +-- 22. Log flyer items that could not be automatically matched to a master item. CREATE TABLE IF NOT EXISTS public.unmatched_flyer_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, @@ -305,7 +378,7 @@ CREATE TABLE IF NOT EXISTS public.unmatched_flyer_items ( COMMENT ON TABLE public.unmatched_flyer_items IS 'A queue for reviewing flyer items that the system failed to automatically match.'; CREATE INDEX IF NOT EXISTS idx_unmatched_flyer_items_flyer_item_id ON public.unmatched_flyer_items(flyer_item_id); --- 19. Store brand information. +-- 23. Store brand information. CREATE TABLE IF NOT EXISTS public.brands ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -317,7 +390,7 @@ CREATE TABLE IF NOT EXISTS public.brands ( COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".'; COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.'; --- 20. For specific products, linking a master item with a brand and size. +-- 24. For specific products, linking a master item with a brand and size. CREATE TABLE IF NOT EXISTS public.products ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id), @@ -336,32 +409,8 @@ COMMENT ON COLUMN public.products.name IS 'Prime Raised without Antibiotics Chic COMMENT ON COLUMN public.products.size IS 'e.g., "4L", "500g".'; CREATE INDEX IF NOT EXISTS idx_products_master_item_id ON public.products(master_item_id); CREATE INDEX IF NOT EXISTS idx_products_brand_id ON public.products(brand_id); --- Add a GIN index to the 'item' column for fast fuzzy text searching. --- This requires the pg_trgm extension. -CREATE INDEX IF NOT EXISTS flyer_items_item_trgm_idx ON public.flyer_items USING GIN (item gin_trgm_ops); -ADD CONSTRAINT flyer_items_product_id_fkey -FOREIGN KEY (product_id) REFERENCES public.products(id); --- 21. Store individual store locations with geographic data. -CREATE TABLE IF NOT EXISTS public.store_locations ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, - address TEXT NOT NULL, - city TEXT, - province_state TEXT, - postal_code TEXT, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - location GEOGRAPHY(Point, 4326) -); -COMMENT ON TABLE public.store_locations IS 'Stores physical locations of stores with geographic data for proximity searches.'; -COMMENT ON COLUMN public.store_locations.location IS 'Geographic coordinates (longitude, latitude) of the store.'; -CREATE INDEX IF NOT EXISTS idx_store_locations_store_id ON public.store_locations(store_id); --- Add a GIST index for efficient geographic queries. --- This requires the postgis extension. -CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location); - --- 22. Linking table if one flyer is valid for multiple locations. +-- 25. Linking table for when one flyer is valid for multiple locations. CREATE TABLE IF NOT EXISTS public.flyer_locations ( flyer_id BIGINT NOT NULL REFERENCES public.flyers(id) ON DELETE CASCADE, store_location_id BIGINT NOT NULL REFERENCES public.store_locations(id) ON DELETE CASCADE, @@ -373,10 +422,10 @@ COMMENT ON TABLE public.flyer_locations IS 'A linking table associating a single CREATE INDEX IF NOT EXISTS idx_flyer_locations_flyer_id ON public.flyer_locations(flyer_id); CREATE INDEX IF NOT EXISTS idx_flyer_locations_store_location_id ON public.flyer_locations(store_location_id); --- 23. Store recipes, which can be user-created or pre-populated. +-- 26. Store recipes, which can be user-created or pre-populated. CREATE TABLE IF NOT EXISTS public.recipes ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + user_id UUID REFERENCES public.users(id) ON DELETE CASCADE, original_recipe_id BIGINT REFERENCES public.recipes(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT, @@ -406,8 +455,11 @@ COMMENT ON COLUMN public.recipes.fat_grams IS 'Optional nutritional information. COMMENT ON COLUMN public.recipes.carb_grams IS 'Optional nutritional information.'; CREATE INDEX IF NOT EXISTS idx_recipes_user_id ON public.recipes(user_id); CREATE INDEX IF NOT EXISTS idx_recipes_original_recipe_id ON public.recipes(original_recipe_id); +-- Add a partial unique index to ensure system-wide recipes (user_id IS NULL) have unique names. +-- This allows different users to have recipes with the same name. +CREATE UNIQUE INDEX IF NOT EXISTS idx_recipes_unique_system_recipe_name ON public.recipes(name) WHERE user_id IS NULL; --- 24. For ingredients required for each recipe. +-- 27. For ingredients required for each recipe. CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -422,7 +474,7 @@ COMMENT ON COLUMN public.recipe_ingredients.unit IS 'e.g., "cups", "tbsp", "g", CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_recipe_id ON public.recipe_ingredients(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_master_item_id ON public.recipe_ingredients(master_item_id); --- 25. Suggest ingredient substitutions for a recipe. +-- 28. Suggest ingredient substitutions for a recipe. CREATE TABLE IF NOT EXISTS public.recipe_ingredient_substitutions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_ingredient_id BIGINT NOT NULL REFERENCES public.recipe_ingredients(id) ON DELETE CASCADE, @@ -436,7 +488,7 @@ COMMENT ON TABLE public.recipe_ingredient_substitutions IS 'Stores suggested alt CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_recipe_ingredient_id ON public.recipe_ingredient_substitutions(recipe_ingredient_id); CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_substitute_master_item_id ON public.recipe_ingredient_substitutions(substitute_master_item_id); --- 26. Store a predefined list of tags for recipes. +-- 29. Store a predefined list of tags for recipes. CREATE TABLE IF NOT EXISTS public.tags ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -445,7 +497,7 @@ CREATE TABLE IF NOT EXISTS public.tags ( ); COMMENT ON TABLE public.tags IS 'Stores tags for categorizing recipes, e.g., "Vegetarian", "Quick & Easy".'; --- 27. Associate multiple tags with a recipe. +-- 30. Associate multiple tags with a recipe. CREATE TABLE IF NOT EXISTS public.recipe_tags ( recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE, @@ -457,7 +509,16 @@ COMMENT ON TABLE public.recipe_tags IS 'A linking table to associate multiple ta CREATE INDEX IF NOT EXISTS idx_recipe_tags_recipe_id ON public.recipe_tags(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_tags_tag_id ON public.recipe_tags(tag_id); --- 28. Associate recipes with required appliances. +-- 31. Store a predefined list of kitchen appliances. +CREATE TABLE IF NOT EXISTS public.appliances ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.appliances IS 'A predefined list of kitchen appliances (e.g., Air Fryer, Instant Pot).'; + +-- 32. Associate recipes with required appliances. CREATE TABLE IF NOT EXISTS public.recipe_appliances ( recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, @@ -469,7 +530,7 @@ COMMENT ON TABLE public.recipe_appliances IS 'Links recipes to the specific kitc CREATE INDEX IF NOT EXISTS idx_recipe_appliances_recipe_id ON public.recipe_appliances(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_appliances_appliance_id ON public.recipe_appliances(appliance_id); --- 29. Store individual user ratings for recipes. +-- 33. Store individual user ratings for recipes. CREATE TABLE IF NOT EXISTS public.recipe_ratings ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -484,7 +545,7 @@ COMMENT ON TABLE public.recipe_ratings IS 'Stores individual user ratings for re CREATE INDEX IF NOT EXISTS idx_recipe_ratings_recipe_id ON public.recipe_ratings(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_ratings_user_id ON public.recipe_ratings(user_id); --- 30. For user comments on recipes to enable discussion. +-- 34. For user comments on recipes to enable discussion. CREATE TABLE IF NOT EXISTS public.recipe_comments ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -501,21 +562,19 @@ CREATE INDEX IF NOT EXISTS idx_recipe_comments_recipe_id ON public.recipe_commen CREATE INDEX IF NOT EXISTS idx_recipe_comments_user_id ON public.recipe_comments(user_id); CREATE INDEX IF NOT EXISTS idx_recipe_comments_parent_comment_id ON public.recipe_comments(parent_comment_id); --- 31. Store a user's collection of planned meals for a date range. -CREATE TABLE IF NOT EXISTS public.menu_plans ( +-- 35. For users to define locations within their pantry. +CREATE TABLE IF NOT EXISTS public.pantry_locations ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - start_date DATE NOT NULL, - end_date DATE NOT NULL, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT date_range_check CHECK (end_date >= start_date) + UNIQUE(user_id, name) ); -COMMENT ON TABLE public.menu_plans IS 'Represents a user''s meal plan for a specific period, e.g., "Week of Oct 23".'; -CREATE INDEX IF NOT EXISTS idx_menu_plans_user_id ON public.menu_plans(user_id); +COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").'; +CREATE INDEX IF NOT EXISTS idx_pantry_locations_user_id ON public.pantry_locations(user_id); --- 32. Associate a recipe with a specific date and meal type within a menu plan. +-- 36. Associate a recipe with a specific date and meal type within a menu plan. CREATE TABLE IF NOT EXISTS public.planned_meals ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, @@ -531,7 +590,7 @@ COMMENT ON COLUMN public.planned_meals.meal_type IS 'The designated meal for the CREATE INDEX IF NOT EXISTS idx_planned_meals_menu_plan_id ON public.planned_meals(menu_plan_id); CREATE INDEX IF NOT EXISTS idx_planned_meals_recipe_id ON public.planned_meals(recipe_id); --- 33. Track the grocery items a user currently has in their pantry. +-- 37. Track the grocery items a user currently has in their pantry. CREATE TABLE IF NOT EXISTS public.pantry_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -552,7 +611,7 @@ CREATE INDEX IF NOT EXISTS idx_pantry_items_user_id ON public.pantry_items(user_ CREATE INDEX IF NOT EXISTS idx_pantry_items_master_item_id ON public.pantry_items(master_item_id); CREATE INDEX IF NOT EXISTS idx_pantry_items_pantry_location_id ON public.pantry_items(pantry_location_id); --- 34. Store password reset tokens. +-- 38. Store password reset tokens. CREATE TABLE IF NOT EXISTS public.password_reset_tokens ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -567,22 +626,7 @@ COMMENT ON COLUMN public.password_reset_tokens.expires_at IS 'The timestamp when CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON public.password_reset_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); --- 35. Store password reset tokens. -CREATE TABLE IF NOT EXISTS public.password_reset_tokens ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, - expires_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.password_reset_tokens IS 'Stores secure, single-use tokens for password reset requests.'; -COMMENT ON COLUMN public.password_reset_tokens.token_hash IS 'A bcrypt hash of the reset token sent to the user.'; -COMMENT ON COLUMN public.password_reset_tokens.expires_at IS 'The timestamp when this token is no longer valid.'; -CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON public.password_reset_tokens(user_id); -CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); - --- 36. Store unit conversion factors for specific master grocery items. +-- 39. Store unit conversion factors for specific master grocery items. CREATE TABLE IF NOT EXISTS public.unit_conversions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -597,7 +641,7 @@ COMMENT ON TABLE public.unit_conversions IS 'Stores item-specific unit conversio COMMENT ON COLUMN public.unit_conversions.factor IS 'The multiplication factor to convert from_unit to to_unit.'; CREATE INDEX IF NOT EXISTS idx_unit_conversions_master_item_id ON public.unit_conversions(master_item_id); --- 37. For users to create their own private aliases for items. +-- 40. For users to create their own private aliases for items. CREATE TABLE IF NOT EXISTS public.user_item_aliases ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -611,7 +655,7 @@ COMMENT ON TABLE public.user_item_aliases IS 'Allows users to create personal al CREATE INDEX IF NOT EXISTS idx_user_item_aliases_user_id ON public.user_item_aliases(user_id); CREATE INDEX IF NOT EXISTS idx_user_item_aliases_master_item_id ON public.user_item_aliases(master_item_id); --- 38. For users to mark their favorite recipes. +-- 41. For users to mark their favorite recipes. CREATE TABLE IF NOT EXISTS public.favorite_recipes ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -623,7 +667,7 @@ COMMENT ON TABLE public.favorite_recipes IS 'A simple linking table for users to CREATE INDEX IF NOT EXISTS idx_favorite_recipes_user_id ON public.favorite_recipes(user_id); CREATE INDEX IF NOT EXISTS idx_favorite_recipes_recipe_id ON public.favorite_recipes(recipe_id); --- 39. For users to mark their favorite stores. +-- 42. For users to mark their favorite stores. CREATE TABLE IF NOT EXISTS public.favorite_stores ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, @@ -635,21 +679,7 @@ COMMENT ON TABLE public.favorite_stores IS 'A simple linking table for users to CREATE INDEX IF NOT EXISTS idx_favorite_stores_user_id ON public.favorite_stores(user_id); CREATE INDEX IF NOT EXISTS idx_favorite_stores_store_id ON public.favorite_stores(store_id); --- 40. Log key user activities for analytics. -CREATE TABLE IF NOT EXISTS public.activity_log ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, - action TEXT NOT NULL, - display_text TEXT NOT NULL, - icon TEXT, - details JSONB, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.activity_log IS 'Logs key user and system actions for auditing and display in an activity feed.'; -CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log(user_id); - --- 41. For users to group recipes into collections. +-- 43. For users to group recipes into collections. CREATE TABLE IF NOT EXISTS public.recipe_collections ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -661,7 +691,7 @@ CREATE TABLE IF NOT EXISTS public.recipe_collections ( COMMENT ON TABLE public.recipe_collections IS 'Allows users to create personal collections of recipes (e.g., "Holiday Baking").'; CREATE INDEX IF NOT EXISTS idx_recipe_collections_user_id ON public.recipe_collections(user_id); --- 42. Associate recipes with a user's collection. +-- 44. Associate recipes with a user's collection. CREATE TABLE IF NOT EXISTS public.recipe_collection_items ( collection_id BIGINT NOT NULL REFERENCES public.recipe_collections(id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -673,19 +703,7 @@ COMMENT ON TABLE public.recipe_collection_items IS 'Links recipes to a user-defi CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_collection_id ON public.recipe_collection_items(collection_id); CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_recipe_id ON public.recipe_collection_items(recipe_id); --- 43. For users to define locations within their pantry. -CREATE TABLE IF NOT EXISTS public.pantry_locations ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - UNIQUE(user_id, name) -); -COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").'; -CREATE INDEX IF NOT EXISTS idx_pantry_locations_user_id ON public.pantry_locations(user_id); - --- 44. Log user search queries for analysis. +-- 45. Log user search queries for analysis. CREATE TABLE IF NOT EXISTS public.search_queries ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, @@ -699,7 +717,7 @@ COMMENT ON TABLE public.search_queries IS 'Logs user search queries to analyze s COMMENT ON COLUMN public.search_queries.was_successful IS 'Indicates if the user interacted with a search result.'; CREATE INDEX IF NOT EXISTS idx_search_queries_user_id ON public.search_queries(user_id); --- 45. Store historical records of completed shopping trips. +-- 46. Store historical records of completed shopping trips. CREATE TABLE IF NOT EXISTS public.shopping_trips ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -713,7 +731,7 @@ COMMENT ON COLUMN public.shopping_trips.total_spent_cents IS 'The total amount s CREATE INDEX IF NOT EXISTS idx_shopping_trips_user_id ON public.shopping_trips(user_id); CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shopping_trips(shopping_list_id); --- 46. Store the items purchased during a specific shopping trip. +-- 47. Store the items purchased during a specific shopping trip. CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, @@ -730,24 +748,7 @@ COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual pri CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_shopping_trip_id ON public.shopping_trip_items(shopping_trip_id); CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_master_item_id ON public.shopping_trip_items(master_item_id); --- 47. Store the items purchased during a specific shopping trip. -CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, - master_item_id BIGINT REFERENCES public.master_grocery_items(id), - custom_item_name TEXT, - quantity NUMERIC NOT NULL, - price_paid_cents INTEGER, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT trip_must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) -); -COMMENT ON TABLE public.shopping_trip_items IS 'A historical log of items purchased during a shopping trip.'; -COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual price paid for the item during the trip, if provided.'; -CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_shopping_trip_id ON public.shopping_trip_items(shopping_trip_id); -CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_master_item_id ON public.shopping_trip_items(master_item_id); - --- 48. Store predefined dietary restrictions (diets and allergies). +-- 48. Store predefined dietary restrictions (diets and allergies). CREATE TABLE IF NOT EXISTS public.dietary_restrictions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -757,7 +758,7 @@ CREATE TABLE IF NOT EXISTS public.dietary_restrictions ( ); COMMENT ON TABLE public.dietary_restrictions IS 'A predefined list of common diets (e.g., Vegan) and allergies (e.g., Nut Allergy).'; --- 49. For a user's specific dietary restrictions. +-- 49. For a user's specific dietary restrictions. CREATE TABLE IF NOT EXISTS public.user_dietary_restrictions ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, restriction_id BIGINT NOT NULL REFERENCES public.dietary_restrictions(id) ON DELETE CASCADE, @@ -769,28 +770,7 @@ COMMENT ON TABLE public.user_dietary_restrictions IS 'Connects users to their se CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_user_id ON public.user_dietary_restrictions(user_id); CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_restriction_id ON public.user_dietary_restrictions(restriction_id); --- 50. For a user's specific dietary restrictions. -CREATE TABLE IF NOT EXISTS public.user_dietary_restrictions ( - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - restriction_id BIGINT NOT NULL REFERENCES public.dietary_restrictions(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, restriction_id), - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.user_dietary_restrictions IS 'Connects users to their selected dietary needs and allergies.'; -CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_user_id ON public.user_dietary_restrictions(user_id); -CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_restriction_id ON public.user_dietary_restrictions(restriction_id); - --- 51. Store a predefined list of kitchen appliances. -CREATE TABLE IF NOT EXISTS public.appliances ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - name TEXT NOT NULL UNIQUE, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.appliances IS 'A predefined list of kitchen appliances (e.g., Air Fryer, Instant Pot).'; - --- 52. For a user's owned kitchen appliances. +-- 50. For a user's owned kitchen appliances. CREATE TABLE IF NOT EXISTS public.user_appliances ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, @@ -802,19 +782,7 @@ COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user CREATE INDEX IF NOT EXISTS idx_user_appliances_user_id ON public.user_appliances(user_id); CREATE INDEX IF NOT EXISTS idx_user_appliances_appliance_id ON public.user_appliances(appliance_id); --- 53. Dor a user's owned kitchen appliances. -CREATE TABLE IF NOT EXISTS public.user_appliances ( - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, appliance_id), - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user owns to help with recipe recommendations.'; -CREATE INDEX IF NOT EXISTS idx_user_appliances_user_id ON public.user_appliances(user_id); -CREATE INDEX IF NOT EXISTS idx_user_appliances_appliance_id ON public.user_appliances(appliance_id); - --- 54. Manage the social graph (following relationships). +-- 51. Manage the social graph (following relationships). CREATE TABLE IF NOT EXISTS public.user_follows ( follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, following_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -827,20 +795,7 @@ COMMENT ON TABLE public.user_follows IS 'Stores user following relationships to CREATE INDEX IF NOT EXISTS idx_user_follows_follower_id ON public.user_follows(follower_id); CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(following_id); --- 55. Manage the social graph (following relationships). -CREATE TABLE IF NOT EXISTS public.user_follows ( - follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - following_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - PRIMARY KEY (follower_id, following_id), - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT cant_follow_self CHECK (follower_id <> following_id) -); -COMMENT ON TABLE public.user_follows IS 'Stores user following relationships to build a social graph.'; -CREATE INDEX IF NOT EXISTS idx_user_follows_follower_id ON public.user_follows(follower_id); -CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(following_id); - --- 56. Store uploaded user receipts for purchase tracking and analysis. +-- 52. Store uploaded user receipts for purchase tracking and analysis. CREATE TABLE IF NOT EXISTS public.receipts ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -858,42 +813,7 @@ COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); --- 57. Store uploaded user receipts for purchase tracking and analysis. -CREATE TABLE IF NOT EXISTS public.receipts ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - store_id BIGINT REFERENCES public.stores(id), - receipt_image_url TEXT NOT NULL, - transaction_date TIMESTAMPTZ, - total_amount_cents INTEGER, - status TEXT DEFAULT 'pending' NOT NULL CHECK (status IN ('pending', 'processing', 'completed', 'failed')), - raw_text TEXT, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - processed_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.'; -CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); -CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); - --- 58. Store individual line items extracted from a user receipt. -CREATE TABLE IF NOT EXISTS public.receipt_items ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - receipt_id BIGINT NOT NULL REFERENCES public.receipts(id) ON DELETE CASCADE, - raw_item_description TEXT NOT NULL, - quantity NUMERIC DEFAULT 1 NOT NULL, - price_paid_cents INTEGER NOT NULL, - master_item_id BIGINT REFERENCES public.master_grocery_items(id), - product_id BIGINT REFERENCES public.products(id), - status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')), - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.receipt_items IS 'Stores individual line items extracted from a user receipt.'; -CREATE INDEX IF NOT EXISTS idx_receipt_items_receipt_id ON public.receipt_items(receipt_id); -CREATE INDEX IF NOT EXISTS idx_receipt_items_master_item_id ON public.receipt_items(master_item_id); - --- 59. Store individual line items extracted from a user receipt. +-- 53. Store individual line items extracted from a user receipt. CREATE TABLE IF NOT EXISTS public.receipt_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, receipt_id BIGINT NOT NULL REFERENCES public.receipts(id) ON DELETE CASCADE, diff --git a/sql/master_schema_rollup.sql b/sql/master_schema_rollup.sql index e39faa3..8441c97 100644 --- a/sql/master_schema_rollup.sql +++ b/sql/master_schema_rollup.sql @@ -16,7 +16,7 @@ -- first run 'delete_all_tables.sql.txt' and then run this script. -- ============================================================================ --- PART 0: EXTENSIONS +-- PART 1: EXTENSIONS -- ============================================================================ -- Enable necessary PostgreSQL extensions. -- postgis: For storing and querying geographic data (store locations). @@ -26,9 +26,9 @@ CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- For generating UUIDs -- ============================================================================ --- PART 0.5: USER AUTHENTICATION TABLE +-- PART 2: TABLES -- ============================================================================ --- This replaces the Supabase `auth.users` table. +-- 1. Users - This replaces the Supabase `auth.users` table. CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT NOT NULL UNIQUE, @@ -46,12 +46,37 @@ COMMENT ON COLUMN public.users.last_failed_login IS 'Timestamp of the last faile -- Add an index on the refresh_token for faster lookups when refreshing tokens. CREATE INDEX IF NOT EXISTS idx_users_refresh_token ON public.users(refresh_token); +-- 2. Log key user activities for analytics. +-- This needs to be created early as many triggers will insert into it. +CREATE TABLE IF NOT EXISTS public.activity_log ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + action TEXT NOT NULL, + display_text TEXT NOT NULL, + icon TEXT, + details JSONB, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.activity_log IS 'Logs key user and system actions for auditing and display in an activity feed.'; +CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log(user_id); --- ============================================================================ --- PART 1: TABLE CREATION --- ============================================================================ +-- 3. for public user profiles. +-- This table is linked to the users table and stores non-sensitive user data. +CREATE TABLE IF NOT EXISTS public.profiles ( + id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE, + full_name TEXT, + avatar_url TEXT, + preferences JSONB, + role TEXT CHECK (role IN ('admin', 'user')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + created_by UUID REFERENCES public.users(id) ON DELETE SET NULL, + updated_by UUID REFERENCES public.users(id) ON DELETE SET NULL +); +COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.'; --- 1. Create the 'stores' table for normalized store data. +-- 4. The 'stores' table for normalized store data. CREATE TABLE IF NOT EXISTS public.stores ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -62,7 +87,7 @@ CREATE TABLE IF NOT EXISTS public.stores ( ); COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).'; --- 2. Create the 'categories' table for normalized category data. +-- 5. The 'categories' table for normalized category data. CREATE TABLE IF NOT EXISTS public.categories ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -71,21 +96,7 @@ CREATE TABLE IF NOT EXISTS public.categories ( ); COMMENT ON TABLE public.categories IS 'Stores a predefined list of grocery item categories (e.g., ''Fruits & Vegetables'', ''Dairy & Eggs'').'; --- 4. Create the 'master_grocery_items' table. This is the master dictionary. -CREATE TABLE IF NOT EXISTS public.master_grocery_items ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - name TEXT NOT NULL UNIQUE, - category_id BIGINT REFERENCES public.categories(id), - is_allergen BOOLEAN DEFAULT false, - allergy_info JSONB, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - created_by UUID REFERENCES public.users(id) ON DELETE SET NULL -); -COMMENT ON TABLE public.master_grocery_items IS 'The master dictionary of canonical grocery items. Each item has a unique name and is linked to a category.'; -CREATE INDEX IF NOT EXISTS idx_master_grocery_items_category_id ON public.master_grocery_items(category_id); - --- 3. Create the 'flyers' table with its full, final schema. +-- 6. flyers' table CREATE TABLE IF NOT EXISTS public.flyers ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, file_name TEXT NOT NULL, @@ -107,60 +118,22 @@ COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.'; --- 6. Create the 'flyer_items' table with its full, final schema. -CREATE TABLE IF NOT EXISTS public.flyer_items ( + +-- 7. The 'master_grocery_items' table. This is the master dictionary. +CREATE TABLE IF NOT EXISTS public.master_grocery_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - flyer_id BIGINT REFERENCES public.flyers(id) ON DELETE CASCADE, - item TEXT NOT NULL, - price_display TEXT NOT NULL, - price_in_cents INTEGER, - quantity TEXT NOT NULL, - quantity_num NUMERIC, - master_item_id BIGINT REFERENCES public.master_grocery_items(id), - view_count INTEGER DEFAULT 0 NOT NULL, - click_count INTEGER DEFAULT 0 NOT NULL, + name TEXT NOT NULL UNIQUE, category_id BIGINT REFERENCES public.categories(id), - category_name TEXT, - unit_price JSONB, - product_id BIGINT -); -COMMENT ON TABLE public.flyer_items IS 'Stores individual items extracted from a specific flyer.'; -CREATE INDEX IF NOT EXISTS idx_flyer_items_flyer_id ON public.flyer_items(flyer_id); -CREATE INDEX IF NOT EXISTS idx_flyer_items_master_item_id ON public.flyer_items(master_item_id); -CREATE INDEX IF NOT EXISTS idx_flyer_items_category_id ON public.flyer_items(category_id); -CREATE INDEX IF NOT EXISTS idx_flyer_items_product_id ON public.flyer_items(product_id); -COMMENT ON COLUMN public.flyer_items.flyer_id IS 'Foreign key linking this item to its parent flyer in the `flyers` table.'; -COMMENT ON COLUMN public.flyer_items.item IS 'The raw item name as it appears in the flyer (e.g., "Granny Smith Apples").'; -COMMENT ON COLUMN public.flyer_items.price_display IS 'The raw price string from the flyer (e.g., "$3.99", "2 for $5.00").'; -COMMENT ON COLUMN public.flyer_items.price_in_cents IS 'The normalized price for a single item, in cents, for easier sorting and comparison.'; -COMMENT ON COLUMN public.flyer_items.quantity IS 'The raw quantity or deal description string (e.g., "per lb", "500g bag").'; -COMMENT ON COLUMN public.flyer_items.quantity_num IS 'The parsed primary numeric value from the quantity string (e.g., 500 from "500g").'; -COMMENT ON COLUMN public.flyer_items.master_item_id IS 'Foreign key linking this flyer item to its canonical entry in `master_grocery_items`. Null if no match was found.'; -COMMENT ON COLUMN public.flyer_items.category_id IS 'Foreign key to the item''s category. Can be redundant if master_item_id is set, but useful.'; -COMMENT ON COLUMN public.flyer_items.category_name IS 'The denormalized name of the category for faster UI display without extra joins.'; -COMMENT ON COLUMN public.flyer_items.unit_price IS 'A JSONB object storing the calculated unit price (e.g., {"value": 1.99, "unit": "lb"}) for standardized price comparisons.'; -COMMENT ON COLUMN public.flyer_items.product_id IS 'A foreign key for future use, to link to a specific product with a UPC code in the `products` table.'; - --- Add a GIN index to the 'item' column for fast fuzzy text searching. --- This requires the pg_trgm extension. -CREATE INDEX IF NOT EXISTS flyer_items_item_trgm_idx ON public.flyer_items USING GIN (item gin_trgm_ops); - --- 0. Create a table for public user profiles. --- This table is linked to the auth.users table and stores non-sensitive user data. -CREATE TABLE IF NOT EXISTS public.profiles ( - id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE, - full_name TEXT, - avatar_url TEXT, - preferences JSONB, - role TEXT CHECK (role IN ('admin', 'user')), + is_allergen BOOLEAN DEFAULT false, + allergy_info JSONB, created_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - created_by UUID REFERENCES public.users(id) ON DELETE SET NULL, - updated_by UUID REFERENCES public.users(id) ON DELETE SET NULL + created_by UUID REFERENCES public.users(id) ON DELETE SET NULL ); -COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the private auth.users table.'; +COMMENT ON TABLE public.master_grocery_items IS 'The master dictionary of canonical grocery items. Each item has a unique name and is linked to a category.'; +CREATE INDEX IF NOT EXISTS idx_master_grocery_items_category_id ON public.master_grocery_items(category_id); --- 5. Create the 'user_watched_items' table. This links to the master list. +-- 8. The 'user_watched_items' table. This links to the master list. CREATE TABLE IF NOT EXISTS public.user_watched_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -172,7 +145,45 @@ CREATE TABLE IF NOT EXISTS public.user_watched_items ( COMMENT ON TABLE public.user_watched_items IS 'A linking table that represents a user''s personal watchlist of grocery items.'; CREATE INDEX IF NOT EXISTS idx_user_watched_items_master_item_id ON public.user_watched_items(master_item_id); --- 7. Create a table for user-defined alerts on watched items. +-- 9. The 'flyer_items' table. This stores individual items from flyers. +CREATE TABLE IF NOT EXISTS public.flyer_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + flyer_id BIGINT REFERENCES public.flyers(id) ON DELETE CASCADE, + item TEXT NOT NULL, + price_display TEXT NOT NULL, + price_in_cents INTEGER, + quantity_num NUMERIC, + quantity TEXT NOT NULL, + category_id BIGINT REFERENCES public.categories(id), + category_name TEXT, + unit_price JSONB, + view_count INTEGER DEFAULT 0 NOT NULL, + click_count INTEGER DEFAULT 0 NOT NULL, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + product_id BIGINT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.flyer_items IS 'Stores individual items extracted from a specific flyer.'; +COMMENT ON COLUMN public.flyer_items.flyer_id IS 'Foreign key linking this item to its parent flyer in the `flyers` table.'; +COMMENT ON COLUMN public.flyer_items.item IS 'The raw item name as it appears in the flyer (e.g., "Granny Smith Apples").'; +COMMENT ON COLUMN public.flyer_items.price_display IS 'The raw price string from the flyer (e.g., "$3.99", "2 for $5.00").'; +COMMENT ON COLUMN public.flyer_items.price_in_cents IS 'The normalized price for a single item, in cents, for easier sorting and comparison.'; +COMMENT ON COLUMN public.flyer_items.quantity IS 'The raw quantity or deal description string (e.g., "per lb", "500g bag").'; +COMMENT ON COLUMN public.flyer_items.quantity_num IS 'The parsed primary numeric value from the quantity string (e.g., 500 from "500g").'; +COMMENT ON COLUMN public.flyer_items.master_item_id IS 'Foreign key linking this flyer item to its canonical entry in `master_grocery_items`. Null if no match was found.'; +COMMENT ON COLUMN public.flyer_items.category_id IS 'Foreign key to the item''s category. Can be redundant if master_item_id is set, but useful.'; +COMMENT ON COLUMN public.flyer_items.category_name IS 'The denormalized name of the category for faster UI display without extra joins.'; +COMMENT ON COLUMN public.flyer_items.unit_price IS 'A JSONB object storing the calculated unit price (e.g., {"value": 1.99, "unit": "lb"}) for standardized price comparisons.'; +COMMENT ON COLUMN public.flyer_items.product_id IS 'A foreign key for future use, to link to a specific product with a UPC code in the `products` table.'; +CREATE INDEX IF NOT EXISTS idx_flyer_items_flyer_id ON public.flyer_items(flyer_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_master_item_id ON public.flyer_items(master_item_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_category_id ON public.flyer_items(category_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_product_id ON public.flyer_items(product_id); +-- Add a GIN index to the 'item' column for fast fuzzy text searching. +-- This requires the pg_trgm extension. +CREATE INDEX IF NOT EXISTS flyer_items_item_trgm_idx ON public.flyer_items USING GIN (item gin_trgm_ops); + +-- 10. For user-defined alerts on watched items. CREATE TABLE IF NOT EXISTS public.user_alerts ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_watched_item_id BIGINT NOT NULL REFERENCES public.user_watched_items(id) ON DELETE CASCADE, @@ -184,11 +195,10 @@ CREATE TABLE IF NOT EXISTS public.user_alerts ( ); COMMENT ON TABLE public.user_alerts IS 'Stores user-configured alert rules for their watched items.'; COMMENT ON COLUMN public.user_alerts.alert_type IS 'The condition that triggers the alert, e.g., ''PRICE_BELOW''.'; -CREATE INDEX IF NOT EXISTS idx_user_alerts_user_watched_item_id ON public.user_alerts(user_watched_item_id); COMMENT ON COLUMN public.user_alerts.threshold_value IS 'The numeric threshold for the alert condition (e.g., price in cents, or percentage).'; +CREATE INDEX IF NOT EXISTS idx_user_alerts_user_watched_item_id ON public.user_alerts(user_watched_item_id); - --- 8. Create a table to store notifications for users. +-- 11. Store notifications for users. CREATE TABLE IF NOT EXISTS public.notifications ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -200,11 +210,29 @@ CREATE TABLE IF NOT EXISTS public.notifications ( ); COMMENT ON TABLE public.notifications IS 'A central log of notifications generated for users, such as price alerts.'; COMMENT ON COLUMN public.notifications.content IS 'The notification message displayed to the user.'; -CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON public.notifications(user_id); COMMENT ON COLUMN public.notifications.link_url IS 'A URL to navigate to when the notification is clicked.'; +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON public.notifications(user_id); +-- 12. Store individual store locations with geographic data. +CREATE TABLE IF NOT EXISTS public.store_locations ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, + address TEXT NOT NULL, + city TEXT, + province_state TEXT, + postal_code TEXT, + location GEOGRAPHY(Point, 4326), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.store_locations IS 'Stores physical locations of stores with geographic data for proximity searches.'; +COMMENT ON COLUMN public.store_locations.location IS 'Geographic coordinates (longitude, latitude) of the store.'; +CREATE INDEX IF NOT EXISTS idx_store_locations_store_id ON public.store_locations(store_id); +-- Add a GIST index for efficient geographic queries. +-- This requires the postgis extension. +CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location); --- 9. Create a table for aggregated, historical price data for master items. +-- 13. For aggregated, historical price data for master items. CREATE TABLE IF NOT EXISTS public.item_price_history ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -220,11 +248,14 @@ CREATE TABLE IF NOT EXISTS public.item_price_history ( ); COMMENT ON TABLE public.item_price_history IS 'Serves as a summary table to speed up charting and analytics.'; COMMENT ON COLUMN public.item_price_history.summary_date IS 'The date for which the price data is summarized.'; +COMMENT ON COLUMN public.item_price_history.min_price_in_cents IS 'The lowest price found for this item on this day, in cents,'; +COMMENT ON COLUMN public.item_price_history.max_price_in_cents IS 'The highest price found for this item on this day, in cents.'; +COMMENT ON COLUMN public.item_price_history.avg_price_in_cents IS 'The average price found for this item on this day, in cents.'; +COMMENT ON COLUMN public.item_price_history.data_points_count IS 'How many data points were used for this summary.'; CREATE INDEX IF NOT EXISTS idx_item_price_history_master_item_id ON public.item_price_history(master_item_id); CREATE INDEX IF NOT EXISTS idx_item_price_history_store_location_id ON public.item_price_history(store_location_id); - --- 10. Create a table to map various names to a single master grocery item. +-- 14. Map various names to a single master grocery item. CREATE TABLE IF NOT EXISTS public.master_item_aliases ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -236,8 +267,7 @@ COMMENT ON TABLE public.master_item_aliases IS 'Stores synonyms or alternative n COMMENT ON COLUMN public.master_item_aliases.alias IS 'An alternative name, e.g., "Ground Chuck" for the master item "Ground Beef".'; CREATE INDEX IF NOT EXISTS idx_master_item_aliases_master_item_id ON public.master_item_aliases(master_item_id); - --- 11. Create tables for user shopping lists. +-- 15. For user shopping lists. CREATE TABLE IF NOT EXISTS public.shopping_lists ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -248,7 +278,7 @@ CREATE TABLE IF NOT EXISTS public.shopping_lists ( COMMENT ON TABLE public.shopping_lists IS 'Stores user-created shopping lists, e.g., "Weekly Groceries".'; CREATE INDEX IF NOT EXISTS idx_shopping_lists_user_id ON public.shopping_lists(user_id); - +-- 16. For items in a user's shopping list. CREATE TABLE IF NOT EXISTS public.shopping_list_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, @@ -263,12 +293,11 @@ CREATE TABLE IF NOT EXISTS public.shopping_list_items ( ); COMMENT ON TABLE public.shopping_list_items IS 'Contains individual items for a specific shopping list.'; COMMENT ON COLUMN public.shopping_list_items.custom_item_name IS 'For items not in the master list, e.g., "Grandma''s special spice mix".'; +COMMENT ON COLUMN public.shopping_list_items.is_purchased IS 'Lets users check items off their list as they shop.'; CREATE INDEX IF NOT EXISTS idx_shopping_list_items_shopping_list_id ON public.shopping_list_items(shopping_list_id); CREATE INDEX IF NOT EXISTS idx_shopping_list_items_master_item_id ON public.shopping_list_items(master_item_id); -COMMENT ON COLUMN public.shopping_list_items.is_purchased IS 'Lets users check items off their list as they shop.'; - --- A table to manage shared access to shopping lists. +-- 17. Manage shared access to shopping lists. CREATE TABLE IF NOT EXISTS public.shared_shopping_lists ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, @@ -284,7 +313,21 @@ CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shopping_list_id ON public. CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_by_user_id ON public.shared_shopping_lists(shared_by_user_id); CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_with_user_id ON public.shared_shopping_lists(shared_with_user_id); --- A table to manage shared access to menu plans. +-- 18. Store a user's collection of planned meals for a date range. +CREATE TABLE IF NOT EXISTS public.menu_plans ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT date_range_check CHECK (end_date >= start_date) +); +COMMENT ON TABLE public.menu_plans IS 'Represents a user''s meal plan for a specific period, e.g., "Week of Oct 23".'; +CREATE INDEX IF NOT EXISTS idx_menu_plans_user_id ON public.menu_plans(user_id); + +-- 19. Manage shared access to menu plans. CREATE TABLE IF NOT EXISTS public.shared_menu_plans ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, @@ -299,7 +342,8 @@ COMMENT ON TABLE public.shared_menu_plans IS 'Allows users to share and collabor CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_menu_plan_id ON public.shared_menu_plans(menu_plan_id); CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_by_user_id ON public.shared_menu_plans(shared_by_user_id); CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_with_user_id ON public.shared_menu_plans(shared_with_user_id); --- 12. Create a table to store user-submitted corrections for flyer items. + +-- 20. Store user-submitted corrections for flyer items. CREATE TABLE IF NOT EXISTS public.suggested_corrections ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, @@ -315,12 +359,11 @@ CREATE TABLE IF NOT EXISTS public.suggested_corrections ( COMMENT ON TABLE public.suggested_corrections IS 'A queue for user-submitted data corrections, enabling crowdsourced data quality improvements.'; COMMENT ON COLUMN public.suggested_corrections.correction_type IS 'The type of error the user is reporting.'; COMMENT ON COLUMN public.suggested_corrections.suggested_value IS 'The corrected value proposed by the user (e.g., a new price or master_item_id).'; +COMMENT ON COLUMN public.suggested_corrections.status IS 'The moderation status of the correction: pending, approved, or rejected.'; CREATE INDEX IF NOT EXISTS idx_suggested_corrections_flyer_item_id ON public.suggested_corrections(flyer_item_id); CREATE INDEX IF NOT EXISTS idx_suggested_corrections_user_id ON public.suggested_corrections(user_id); -COMMENT ON COLUMN public.suggested_corrections.status IS 'The moderation status of the correction: pending, approved, or rejected.'; - --- 13. Create a table for prices submitted directly by users from in-store. +-- 21. For prices submitted directly by users from in-store. CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id), @@ -335,11 +378,11 @@ CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( ); COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.'; COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.'; +COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.'; CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_user_id ON public.user_submitted_prices(user_id); CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_master_item_id ON public.user_submitted_prices(master_item_id); -COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.'; --- A table to log flyer items that could not be automatically matched to a master item. +-- 22. Log flyer items that could not be automatically matched to a master item. CREATE TABLE IF NOT EXISTS public.unmatched_flyer_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, @@ -352,7 +395,7 @@ CREATE TABLE IF NOT EXISTS public.unmatched_flyer_items ( COMMENT ON TABLE public.unmatched_flyer_items IS 'A queue for reviewing flyer items that the system failed to automatically match.'; CREATE INDEX IF NOT EXISTS idx_unmatched_flyer_items_flyer_item_id ON public.unmatched_flyer_items(flyer_item_id); --- A table to store brand information. +-- 23. Store brand information. CREATE TABLE IF NOT EXISTS public.brands ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -364,8 +407,7 @@ CREATE TABLE IF NOT EXISTS public.brands ( COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".'; COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.'; - --- A table for specific products, linking a master item with a brand and size. +-- 24. For specific products, linking a master item with a brand and size. CREATE TABLE IF NOT EXISTS public.products ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id), @@ -379,37 +421,13 @@ CREATE TABLE IF NOT EXISTS public.products ( ); COMMENT ON TABLE public.products IS 'Represents a specific, sellable product, combining a generic item with a brand and size.'; COMMENT ON COLUMN public.products.upc_code IS 'Universal Product Code, if available, for exact product matching.'; +COMMENT ON COLUMN public.products.brand_id IS 'Can be null for generic/store-brand items.'; +COMMENT ON COLUMN public.products.name IS 'Prime Raised without Antibiotics Chicken Breast.'; +COMMENT ON COLUMN public.products.size IS 'e.g., "4L", "500g".'; CREATE INDEX IF NOT EXISTS idx_products_master_item_id ON public.products(master_item_id); CREATE INDEX IF NOT EXISTS idx_products_brand_id ON public.products(brand_id); --- Link flyer_items to the new products table. --- This is done via ALTER TABLE because 'products' is created after 'flyer_items'. -ADD CONSTRAINT flyer_items_product_id_fkey -FOREIGN KEY (product_id) REFERENCES public.products(id); - - --- A table to store individual store locations with geographic data. -CREATE TABLE IF NOT EXISTS public.store_locations ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - store_id BIGINT NOT NULL REFERENCES public.stores(id), - address TEXT NOT NULL, - city TEXT, - province_state TEXT, - postal_code TEXT, - location GEOGRAPHY(Point, 4326), - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.store_locations IS 'Stores physical locations of stores with geographic data for proximity searches.'; -COMMENT ON COLUMN public.store_locations.location IS 'Geographic coordinates (longitude, latitude) of the store.'; -CREATE INDEX IF NOT EXISTS idx_store_locations_store_id ON public.store_locations(store_id); - --- Add a GIST index for efficient geographic queries. --- This requires the postgis extension. -CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location); - - --- A linking table if one flyer is valid for multiple locations. +-- 25. Linking table for when one flyer is valid for multiple locations. CREATE TABLE IF NOT EXISTS public.flyer_locations ( flyer_id BIGINT NOT NULL REFERENCES public.flyers(id) ON DELETE CASCADE, store_location_id BIGINT NOT NULL REFERENCES public.store_locations(id) ON DELETE CASCADE, @@ -421,11 +439,11 @@ COMMENT ON TABLE public.flyer_locations IS 'A linking table associating a single CREATE INDEX IF NOT EXISTS idx_flyer_locations_flyer_id ON public.flyer_locations(flyer_id); CREATE INDEX IF NOT EXISTS idx_flyer_locations_store_location_id ON public.flyer_locations(store_location_id); --- A table to store recipes, which can be user-created or pre-populated. +-- 26. Store recipes, which can be user-created or pre-populated. CREATE TABLE IF NOT EXISTS public.recipes ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID REFERENCES public.users(id) ON DELETE CASCADE, - original_recipe_id BIGINT REFERENCES public.recipes(id) ON DELETE SET NULL, -- For forked/variation recipes + original_recipe_id BIGINT REFERENCES public.recipes(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT, instructions TEXT, @@ -445,12 +463,20 @@ CREATE TABLE IF NOT EXISTS public.recipes ( ); COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.'; COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.'; +COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.'; +COMMENT ON COLUMN public.recipes.user_id IS 'Can be a system recipe (user_id is NULL) or user-submitted.'; +COMMENT ON COLUMN public.recipes.avg_rating IS 'Aggregated rating data for fast sorting/display.'; +COMMENT ON COLUMN public.recipes.calories_per_serving IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.protein_grams IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.fat_grams IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.carb_grams IS 'Optional nutritional information.'; CREATE INDEX IF NOT EXISTS idx_recipes_user_id ON public.recipes(user_id); CREATE INDEX IF NOT EXISTS idx_recipes_original_recipe_id ON public.recipes(original_recipe_id); -COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.'; +-- Add a partial unique index to ensure system-wide recipes (user_id IS NULL) have unique names. +-- This allows different users to have recipes with the same name. +CREATE UNIQUE INDEX IF NOT EXISTS idx_recipes_unique_system_recipe_name ON public.recipes(name) WHERE user_id IS NULL; - --- A linking table for ingredients required for each recipe. +-- 27. For ingredients required for each recipe. CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -461,10 +487,11 @@ CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( updated_at TIMESTAMPTZ DEFAULT now() NOT NULL ); COMMENT ON TABLE public.recipe_ingredients IS 'Defines the ingredients and quantities needed for a recipe.'; +COMMENT ON COLUMN public.recipe_ingredients.unit IS 'e.g., "cups", "tbsp", "g", "each".'; CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_recipe_id ON public.recipe_ingredients(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_master_item_id ON public.recipe_ingredients(master_item_id); --- A table to suggest ingredient substitutions for a recipe. +-- 28. Suggest ingredient substitutions for a recipe. CREATE TABLE IF NOT EXISTS public.recipe_ingredient_substitutions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_ingredient_id BIGINT NOT NULL REFERENCES public.recipe_ingredients(id) ON DELETE CASCADE, @@ -478,7 +505,7 @@ COMMENT ON TABLE public.recipe_ingredient_substitutions IS 'Stores suggested alt CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_recipe_ingredient_id ON public.recipe_ingredient_substitutions(recipe_ingredient_id); CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_substitute_master_item_id ON public.recipe_ingredient_substitutions(substitute_master_item_id); --- A table to store a predefined list of tags for recipes. +-- 29. Store a predefined list of tags for recipes. CREATE TABLE IF NOT EXISTS public.tags ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -487,8 +514,7 @@ CREATE TABLE IF NOT EXISTS public.tags ( ); COMMENT ON TABLE public.tags IS 'Stores tags for categorizing recipes, e.g., "Vegetarian", "Quick & Easy".'; - --- A linking table to associate multiple tags with a recipe. +-- 30. Associate multiple tags with a recipe. CREATE TABLE IF NOT EXISTS public.recipe_tags ( recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE, @@ -500,7 +526,16 @@ COMMENT ON TABLE public.recipe_tags IS 'A linking table to associate multiple ta CREATE INDEX IF NOT EXISTS idx_recipe_tags_recipe_id ON public.recipe_tags(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_tags_tag_id ON public.recipe_tags(tag_id); --- A linking table to associate recipes with required appliances. +-- 31. Store a predefined list of kitchen appliances. +CREATE TABLE IF NOT EXISTS public.appliances ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.appliances IS 'A predefined list of kitchen appliances (e.g., Air Fryer, Instant Pot).'; + +-- 32. Associate recipes with required appliances. CREATE TABLE IF NOT EXISTS public.recipe_appliances ( recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, @@ -512,7 +547,7 @@ COMMENT ON TABLE public.recipe_appliances IS 'Links recipes to the specific kitc CREATE INDEX IF NOT EXISTS idx_recipe_appliances_recipe_id ON public.recipe_appliances(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_appliances_appliance_id ON public.recipe_appliances(appliance_id); --- A table to store individual user ratings for recipes. +-- 33. Store individual user ratings for recipes. CREATE TABLE IF NOT EXISTS public.recipe_ratings ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -527,36 +562,36 @@ COMMENT ON TABLE public.recipe_ratings IS 'Stores individual user ratings for re CREATE INDEX IF NOT EXISTS idx_recipe_ratings_recipe_id ON public.recipe_ratings(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_ratings_user_id ON public.recipe_ratings(user_id); --- A table for user comments on recipes to enable discussion. +-- 34. For user comments on recipes to enable discussion. CREATE TABLE IF NOT EXISTS public.recipe_comments ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - parent_comment_id BIGINT REFERENCES public.recipe_comments(id) ON DELETE CASCADE, -- For threaded comments + parent_comment_id BIGINT REFERENCES public.recipe_comments(id) ON DELETE CASCADE, content TEXT NOT NULL, status TEXT DEFAULT 'visible' NOT NULL CHECK (status IN ('visible', 'hidden', 'reported')), created_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL ); COMMENT ON TABLE public.recipe_comments IS 'Allows for threaded discussions and comments on recipes.'; +COMMENT ON COLUMN public.recipe_comments.parent_comment_id IS 'For threaded comments.'; CREATE INDEX IF NOT EXISTS idx_recipe_comments_recipe_id ON public.recipe_comments(recipe_id); CREATE INDEX IF NOT EXISTS idx_recipe_comments_user_id ON public.recipe_comments(user_id); CREATE INDEX IF NOT EXISTS idx_recipe_comments_parent_comment_id ON public.recipe_comments(parent_comment_id); --- A table to store a user's collection of planned meals for a date range. -CREATE TABLE IF NOT EXISTS public.menu_plans ( + +-- 35. For users to define locations within their pantry. +CREATE TABLE IF NOT EXISTS public.pantry_locations ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - start_date DATE NOT NULL, - end_date DATE NOT NULL, + name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(user_id, name) ); -COMMENT ON TABLE public.menu_plans IS 'Represents a user''s meal plan for a specific period, e.g., "Week of Oct 23".'; -CREATE INDEX IF NOT EXISTS idx_menu_plans_user_id ON public.menu_plans(user_id); +COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").'; +CREATE INDEX IF NOT EXISTS idx_pantry_locations_user_id ON public.pantry_locations(user_id); - --- A table to associate a recipe with a specific date and meal type within a menu plan. +-- 36. Associate a recipe with a specific date and meal type within a menu plan. CREATE TABLE IF NOT EXISTS public.planned_meals ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, @@ -572,8 +607,7 @@ COMMENT ON COLUMN public.planned_meals.meal_type IS 'The designated meal for the CREATE INDEX IF NOT EXISTS idx_planned_meals_menu_plan_id ON public.planned_meals(menu_plan_id); CREATE INDEX IF NOT EXISTS idx_planned_meals_recipe_id ON public.planned_meals(recipe_id); - --- A table to track the grocery items a user currently has in their pantry. +-- 37. Track the grocery items a user currently has in their pantry. CREATE TABLE IF NOT EXISTS public.pantry_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -588,13 +622,13 @@ CREATE TABLE IF NOT EXISTS public.pantry_items ( ); COMMENT ON TABLE public.pantry_items IS 'Tracks a user''s personal inventory of grocery items to enable smart shopping lists.'; COMMENT ON COLUMN public.pantry_items.quantity IS 'The current amount of the item. Convention: use grams for weight, mL for volume where applicable.'; +COMMENT ON COLUMN public.pantry_items.pantry_location_id IS 'Links the item to a user-defined location like "Fridge" or "Freezer".'; +COMMENT ON COLUMN public.pantry_items.unit IS 'e.g., ''g'', ''ml'', ''items''. Should align with recipe_ingredients.unit and quantity convention.'; CREATE INDEX IF NOT EXISTS idx_pantry_items_user_id ON public.pantry_items(user_id); CREATE INDEX IF NOT EXISTS idx_pantry_items_master_item_id ON public.pantry_items(master_item_id); CREATE INDEX IF NOT EXISTS idx_pantry_items_pantry_location_id ON public.pantry_items(pantry_location_id); -COMMENT ON COLUMN public.pantry_items.unit IS 'e.g., ''g'', ''ml'', ''items''. Should align with recipe_ingredients.unit and quantity convention.'; -COMMENT ON COLUMN public.pantry_items.pantry_location_id IS 'Links the item to a user-defined location like "Fridge" or "Freezer".'; --- A table to store password reset tokens. +-- 38. Store password reset tokens. CREATE TABLE IF NOT EXISTS public.password_reset_tokens ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -605,13 +639,11 @@ CREATE TABLE IF NOT EXISTS public.password_reset_tokens ( ); COMMENT ON TABLE public.password_reset_tokens IS 'Stores secure, single-use tokens for password reset requests.'; COMMENT ON COLUMN public.password_reset_tokens.token_hash IS 'A bcrypt hash of the reset token sent to the user.'; +COMMENT ON COLUMN public.password_reset_tokens.expires_at IS 'The timestamp when this token is no longer valid.'; CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON public.password_reset_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); -COMMENT ON COLUMN public.password_reset_tokens.expires_at IS 'The timestamp when this token is no longer valid.'; -CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); - --- A table to store unit conversion factors for specific master grocery items. +-- 39. Store unit conversion factors for specific master grocery items. CREATE TABLE IF NOT EXISTS public.unit_conversions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, @@ -626,7 +658,7 @@ COMMENT ON TABLE public.unit_conversions IS 'Stores item-specific unit conversio COMMENT ON COLUMN public.unit_conversions.factor IS 'The multiplication factor to convert from_unit to to_unit.'; CREATE INDEX IF NOT EXISTS idx_unit_conversions_master_item_id ON public.unit_conversions(master_item_id); --- A table for users to create their own private aliases for items. +-- 40. For users to create their own private aliases for items. CREATE TABLE IF NOT EXISTS public.user_item_aliases ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -640,7 +672,7 @@ COMMENT ON TABLE public.user_item_aliases IS 'Allows users to create personal al CREATE INDEX IF NOT EXISTS idx_user_item_aliases_user_id ON public.user_item_aliases(user_id); CREATE INDEX IF NOT EXISTS idx_user_item_aliases_master_item_id ON public.user_item_aliases(master_item_id); --- A table for users to mark their favorite recipes. +-- 41. For users to mark their favorite recipes. CREATE TABLE IF NOT EXISTS public.favorite_recipes ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -652,7 +684,7 @@ COMMENT ON TABLE public.favorite_recipes IS 'A simple linking table for users to CREATE INDEX IF NOT EXISTS idx_favorite_recipes_user_id ON public.favorite_recipes(user_id); CREATE INDEX IF NOT EXISTS idx_favorite_recipes_recipe_id ON public.favorite_recipes(recipe_id); --- A table for users to mark their favorite stores. +-- 42. For users to mark their favorite stores. CREATE TABLE IF NOT EXISTS public.favorite_stores ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, @@ -664,21 +696,7 @@ COMMENT ON TABLE public.favorite_stores IS 'A simple linking table for users to CREATE INDEX IF NOT EXISTS idx_favorite_stores_user_id ON public.favorite_stores(user_id); CREATE INDEX IF NOT EXISTS idx_favorite_stores_store_id ON public.favorite_stores(store_id); --- A generic table to log key user activities for analytics. -CREATE TABLE IF NOT EXISTS public.activity_log ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, - action TEXT NOT NULL, - display_text TEXT NOT NULL, - icon TEXT, - details JSONB, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.activity_log IS 'Logs key user and system actions for auditing and display in an activity feed.'; -CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log(user_id); - --- A table for users to group recipes into collections. +-- 43. For users to group recipes into collections. CREATE TABLE IF NOT EXISTS public.recipe_collections ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -690,7 +708,7 @@ CREATE TABLE IF NOT EXISTS public.recipe_collections ( COMMENT ON TABLE public.recipe_collections IS 'Allows users to create personal collections of recipes (e.g., "Holiday Baking").'; CREATE INDEX IF NOT EXISTS idx_recipe_collections_user_id ON public.recipe_collections(user_id); --- A linking table to associate recipes with a user's collection. +-- 44. Associate recipes with a user's collection. CREATE TABLE IF NOT EXISTS public.recipe_collection_items ( collection_id BIGINT NOT NULL REFERENCES public.recipe_collections(id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, @@ -702,7 +720,52 @@ COMMENT ON TABLE public.recipe_collection_items IS 'Links recipes to a user-defi CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_collection_id ON public.recipe_collection_items(collection_id); CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_recipe_id ON public.recipe_collection_items(recipe_id); --- A table to store predefined dietary restrictions (diets and allergies). +-- 45. Log user search queries for analysis. +CREATE TABLE IF NOT EXISTS public.search_queries ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + query_text TEXT NOT NULL, + result_count INTEGER, + was_successful BOOLEAN, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.search_queries IS 'Logs user search queries to analyze search effectiveness and identify gaps in data.'; +COMMENT ON COLUMN public.search_queries.was_successful IS 'Indicates if the user interacted with a search result.'; +CREATE INDEX IF NOT EXISTS idx_search_queries_user_id ON public.search_queries(user_id); + +-- 46. Store historical records of completed shopping trips. +CREATE TABLE IF NOT EXISTS public.shopping_trips ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + shopping_list_id BIGINT REFERENCES public.shopping_lists(id) ON DELETE SET NULL, + completed_at TIMESTAMPTZ DEFAULT now() NOT NULL, + total_spent_cents INTEGER, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.shopping_trips IS 'A historical record of a completed shopping trip.'; +COMMENT ON COLUMN public.shopping_trips.total_spent_cents IS 'The total amount spent on this shopping trip, if provided by the user.'; +CREATE INDEX IF NOT EXISTS idx_shopping_trips_user_id ON public.shopping_trips(user_id); +CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shopping_trips(shopping_list_id); + +-- 47. Store the items purchased during a specific shopping trip. +CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + custom_item_name TEXT, + quantity NUMERIC NOT NULL, + price_paid_cents INTEGER, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT trip_must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) +); +COMMENT ON TABLE public.shopping_trip_items IS 'A historical log of items purchased during a shopping trip.'; +COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual price paid for the item during the trip, if provided.'; +CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_shopping_trip_id ON public.shopping_trip_items(shopping_trip_id); +CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_master_item_id ON public.shopping_trip_items(master_item_id); + +-- 48. Store predefined dietary restrictions (diets and allergies). CREATE TABLE IF NOT EXISTS public.dietary_restrictions ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name TEXT NOT NULL UNIQUE, @@ -712,7 +775,7 @@ CREATE TABLE IF NOT EXISTS public.dietary_restrictions ( ); COMMENT ON TABLE public.dietary_restrictions IS 'A predefined list of common diets (e.g., Vegan) and allergies (e.g., Nut Allergy).'; --- A linking table for a user's specific dietary restrictions. +-- 49. For a user's specific dietary restrictions. CREATE TABLE IF NOT EXISTS public.user_dietary_restrictions ( user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, restriction_id BIGINT NOT NULL REFERENCES public.dietary_restrictions(id) ON DELETE CASCADE, @@ -724,7 +787,32 @@ COMMENT ON TABLE public.user_dietary_restrictions IS 'Connects users to their se CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_user_id ON public.user_dietary_restrictions(user_id); CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_restriction_id ON public.user_dietary_restrictions(restriction_id); --- A table to store uploaded user receipts for purchase tracking and analysis. +-- 50. For a user's owned kitchen appliances. +CREATE TABLE IF NOT EXISTS public.user_appliances ( + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, appliance_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user owns to help with recipe recommendations.'; +CREATE INDEX IF NOT EXISTS idx_user_appliances_user_id ON public.user_appliances(user_id); +CREATE INDEX IF NOT EXISTS idx_user_appliances_appliance_id ON public.user_appliances(appliance_id); + +-- 51. Manage the social graph (following relationships). +CREATE TABLE IF NOT EXISTS public.user_follows ( + follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + following_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + PRIMARY KEY (follower_id, following_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT cant_follow_self CHECK (follower_id <> following_id) +); +COMMENT ON TABLE public.user_follows IS 'Stores user following relationships to build a social graph.'; +CREATE INDEX IF NOT EXISTS idx_user_follows_follower_id ON public.user_follows(follower_id); +CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(following_id); + +-- 52. Store uploaded user receipts for purchase tracking and analysis. CREATE TABLE IF NOT EXISTS public.receipts ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -742,7 +830,7 @@ COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); --- A table to store individual line items extracted from a user receipt. +-- 53. Store individual line items extracted from a user receipt. CREATE TABLE IF NOT EXISTS public.receipt_items ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, receipt_id BIGINT NOT NULL REFERENCES public.receipts(id) ON DELETE CASCADE, @@ -759,124 +847,6 @@ COMMENT ON TABLE public.receipt_items IS 'Stores individual line items extracted CREATE INDEX IF NOT EXISTS idx_receipt_items_receipt_id ON public.receipt_items(receipt_id); CREATE INDEX IF NOT EXISTS idx_receipt_items_master_item_id ON public.receipt_items(master_item_id); --- A table to store a predefined list of kitchen appliances. -CREATE TABLE IF NOT EXISTS public.appliances ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - name TEXT NOT NULL UNIQUE, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.appliances IS 'A predefined list of kitchen appliances (e.g., Air Fryer, Instant Pot).'; - --- A linking table for a user's owned kitchen appliances. -CREATE TABLE IF NOT EXISTS public.user_appliances ( - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, appliance_id), - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user owns to help with recipe recommendations.'; -CREATE INDEX IF NOT EXISTS idx_user_appliances_user_id ON public.user_appliances(user_id); -CREATE INDEX IF NOT EXISTS idx_user_appliances_appliance_id ON public.user_appliances(appliance_id); - --- A table to manage the social graph (following relationships). -CREATE TABLE IF NOT EXISTS public.user_follows ( - follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - following_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - PRIMARY KEY (follower_id, following_id), - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT cant_follow_self CHECK (follower_id <> following_id) -); -COMMENT ON TABLE public.user_follows IS 'Stores user following relationships to build a social graph.'; -CREATE INDEX IF NOT EXISTS idx_user_follows_follower_id ON public.user_follows(follower_id); -CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(following_id); - --- A table for users to define locations within their pantry. -CREATE TABLE IF NOT EXISTS public.pantry_locations ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - UNIQUE(user_id, name) -); -COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").'; -CREATE INDEX IF NOT EXISTS idx_pantry_locations_user_id ON public.pantry_locations(user_id); - --- A table to log user search queries for analysis. -CREATE TABLE IF NOT EXISTS public.search_queries ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, - query_text TEXT NOT NULL, - result_count INTEGER, - was_successful BOOLEAN, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.search_queries IS 'Logs user search queries to analyze search effectiveness and identify gaps in data.'; -COMMENT ON COLUMN public.search_queries.was_successful IS 'Indicates if the user interacted with a search result.'; -CREATE INDEX IF NOT EXISTS idx_search_queries_user_id ON public.search_queries(user_id); - --- A table to store historical records of completed shopping trips. -CREATE TABLE IF NOT EXISTS public.shopping_trips ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - shopping_list_id BIGINT REFERENCES public.shopping_lists(id) ON DELETE SET NULL, - completed_at TIMESTAMPTZ DEFAULT now() NOT NULL, - total_spent_cents INTEGER, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.shopping_trips IS 'A historical record of a completed shopping trip.'; -COMMENT ON COLUMN public.shopping_trips.total_spent_cents IS 'The total amount spent on this shopping trip, if provided by the user.'; - --- A table to store the items purchased during a specific shopping trip. -CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, - master_item_id BIGINT REFERENCES public.master_grocery_items(id), - custom_item_name TEXT, - quantity NUMERIC NOT NULL, - price_paid_cents INTEGER, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT trip_must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) -); -COMMENT ON TABLE public.shopping_trip_items IS 'A historical log of items purchased during a shopping trip.'; -COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual price paid for the item during the trip, if provided.'; - - - - --- A table to store historical records of completed shopping trips. -CREATE TABLE IF NOT EXISTS public.shopping_trips ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, - shopping_list_id BIGINT REFERENCES public.shopping_lists(id) ON DELETE SET NULL, - completed_at TIMESTAMPTZ DEFAULT now() NOT NULL, - total_spent_cents INTEGER, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL -); -COMMENT ON TABLE public.shopping_trips IS 'A historical record of a completed shopping trip.'; -COMMENT ON COLUMN public.shopping_trips.total_spent_cents IS 'The total amount spent on this shopping trip, if provided by the user.'; -CREATE INDEX IF NOT EXISTS idx_shopping_trips_user_id ON public.shopping_trips(user_id); -CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shopping_trips(shopping_list_id); - --- A table to store the items purchased during a specific shopping trip. -CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, - master_item_id BIGINT REFERENCES public.master_grocery_items(id), - custom_item_name TEXT, - quantity NUMERIC NOT NULL, - price_paid_cents INTEGER, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, - CONSTRAINT trip_must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) -); -COMMENT ON TABLE public.shopping_trip_items IS 'A historical log of items purchased during a shopping trip.'; -COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual price paid for the item during the trip, if provided.'; -- ============================================================================ -- PART 2: DATA SEEDING @@ -968,8 +938,8 @@ BEGIN INSERT INTO public.recipes (name, description, instructions, prep_time_minutes, cook_time_minutes, servings) VALUES ('Simple Chicken and Rice', 'A quick and healthy weeknight meal with chicken, rice, and broccoli.', '1. Cook rice according to package directions. 2. Steam broccoli. 3. Pan-sear chicken breast until cooked through. 4. Combine and serve.', 10, 20, 4), ('Classic Spaghetti Bolognese', 'A rich and hearty meat sauce served over spaghetti, perfect for the whole family.', '1. Brown ground beef with onions and garlic. 2. Add tomatoes and simmer for 30 minutes. 3. Cook pasta. 4. Serve sauce over pasta.', 15, 45, 6), - ('Vegetable Stir-fry', 'A fast, flavorful, and vegetarian stir-fry loaded with fresh vegetables.', '1. Chop all vegetables. 2. Heat oil in a wok or large pan. 3. Stir-fry vegetables for 5-7 minutes until tender-crisp. 4. Add soy sauce and serve immediately.', 10, 10, 3) - ON CONFLICT (name) DO NOTHING; + ('Vegetable Stir-fry', 'A fast, flavorful, and vegetarian stir-fry loaded with fresh vegetables.', '1. Chop all vegetables. 2. Heat oil in a wok or large pan. 3. Stir-fry vegetables for 5-7 minutes until tender-crisp. 4. Add soy sauce and serve immediately.', 10, 10, 3) + ON CONFLICT (name) WHERE user_id IS NULL DO NOTHING; SELECT id INTO chicken_recipe_id FROM public.recipes WHERE name = 'Simple Chicken and Rice'; SELECT id INTO bolognese_recipe_id FROM public.recipes WHERE name = 'Classic Spaghetti Bolognese'; @@ -1687,25 +1657,31 @@ AS $$ $$; -- Function to get a paginated list of recent activities for the audit log. +-- We drop it first because the return signature has changed. +DROP FUNCTION IF EXISTS public.get_activity_log(integer, integer); + CREATE OR REPLACE FUNCTION public.get_activity_log(p_limit INTEGER DEFAULT 20, p_offset INTEGER DEFAULT 0) RETURNS TABLE ( id BIGINT, user_id UUID, - action TEXT, - display_text TEXT, - icon TEXT, + action TEXT, -- Changed from activity_type + display_text TEXT, -- Added + icon TEXT, -- Added details JSONB, - created_at TIMESTAMPTZ + created_at TIMESTAMPTZ, + user_full_name TEXT, -- Added + user_avatar_url TEXT -- Added ) LANGUAGE sql STABLE SECURITY INVOKER AS $$ SELECT - al.id, al.user_id, al.action, al.display_text, al.icon, al.details, al.created_at + al.id, al.user_id, al.action, al.display_text, al.icon, al.details, al.created_at, + p.full_name, p.avatar_url FROM public.activity_log al - ORDER BY - al.created_at DESC + LEFT JOIN public.profiles p ON al.user_id = p.id + ORDER BY al.created_at DESC LIMIT p_limit OFFSET p_offset; $$; @@ -1740,36 +1716,6 @@ AS $$ WHERE u.id = p_user_id; $$; --- Function to get a paginated list of recent activities for the audit log (with user details) -CREATE OR REPLACE FUNCTION public.get_activity_log_with_users(p_limit INTEGER DEFAULT 20, p_offset INTEGER DEFAULT 0) -RETURNS TABLE ( - id BIGINT, - user_id UUID, - action TEXT, - display_text TEXT, - icon TEXT, - details JSONB, - created_at TIMESTAMPTZ, - user_full_name TEXT, - user_avatar_url TEXT -) -LANGUAGE sql -STABLE -SECURITY INVOKER -AS $$ - SELECT - al.id, al.user_id, al.action, al.display_text, al.icon, al.details, al.created_at, - p.full_name, p.avatar_url - FROM public.activity_log al - -- Join with profiles to get user details for display. - -- LEFT JOIN is used because some activities might be system-generated (user_id is NULL). - LEFT JOIN public.profiles p ON al.user_id = p.id - ORDER BY - al.created_at DESC - LIMIT p_limit - OFFSET p_offset; -$$; - -- Function to get recipes that are compatible with a user's dietary restrictions (allergies). -- It filters out any recipe containing an ingredient that the user is allergic to. CREATE OR REPLACE FUNCTION public.get_recipes_for_user_diets(p_user_id UUID) @@ -1831,7 +1777,7 @@ AS $$ JOIN public.profiles p ON al.user_id = p.id WHERE al.user_id IN (SELECT following_id FROM FollowedUsers) - -- We can filter for specific activity types to make the feed more relevant. + -- We can filter for specific action types to make the feed more relevant. AND al.action IN ( 'recipe_created', 'recipe_favorited', @@ -2003,6 +1949,10 @@ AS $$ SELECT * FROM public.fork_recipe(p_user_id, p_original_recipe_id); $$; + + + + -- ============================================================================ -- PART 7: TRIGGERS -- ============================================================================ @@ -2059,7 +2009,6 @@ CREATE TRIGGER on_profile_updated BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); --- Apply the trigger to all other tables that have an 'updated_at' column. DROP TRIGGER IF EXISTS on_users_updated ON public.users; CREATE TRIGGER on_users_updated BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); @@ -2087,6 +2036,31 @@ CREATE TRIGGER on_notifications_updated BEFORE UPDATE ON public.notifications FO DROP TRIGGER IF EXISTS on_item_price_history_updated ON public.item_price_history; CREATE TRIGGER on_item_price_history_updated BEFORE UPDATE ON public.item_price_history FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); +DROP TRIGGER IF EXISTS on_menu_plans_updated ON public.menu_plans; +CREATE TRIGGER on_menu_plans_updated BEFORE UPDATE ON public.menu_plans FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_shared_shopping_lists_updated ON public.shared_shopping_lists; +CREATE TRIGGER on_shared_shopping_lists_updated BEFORE UPDATE ON public.shared_shopping_lists FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_shared_menu_plans_updated ON public.shared_menu_plans; +CREATE TRIGGER on_shared_menu_plans_updated BEFORE UPDATE ON public.shared_menu_plans FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_favorite_recipes_updated ON public.favorite_recipes; +CREATE TRIGGER on_favorite_recipes_updated BEFORE UPDATE ON public.favorite_recipes FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +-- Apply the trigger to all other tables that have an 'updated_at' column. +DROP TRIGGER IF EXISTS on_shared_shopping_lists_updated ON public.shared_shopping_lists; +CREATE TRIGGER on_shared_shopping_lists_updated BEFORE UPDATE ON public.shared_shopping_lists FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_shared_menu_plans_updated ON public.shared_menu_plans; +CREATE TRIGGER on_shared_menu_plans_updated BEFORE UPDATE ON public.shared_menu_plans FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_favorite_recipes_updated ON public.favorite_recipes; +CREATE TRIGGER on_favorite_recipes_updated BEFORE UPDATE ON public.favorite_recipes FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + +DROP TRIGGER IF EXISTS on_user_follows_updated ON public.user_follows; +CREATE TRIGGER on_user_follows_updated BEFORE UPDATE ON public.user_follows FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); + -- (Apply to other tables as needed...) -- Apply the trigger to the 'pantry_items' table. @@ -2271,7 +2245,9 @@ $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS on_new_recipe_created ON public.recipes; CREATE TRIGGER on_new_recipe_created AFTER INSERT ON public.recipes - FOR EACH ROW EXECUTE FUNCTION public.log_new_recipe(); + FOR EACH ROW + WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes. + EXECUTE FUNCTION public.log_new_recipe(); -- 7. Trigger function to log the creation of a new flyer. CREATE OR REPLACE FUNCTION public.log_new_flyer() diff --git a/sql/temp-mas-for-compare.sql b/sql/temp-mas-for-compare.sql new file mode 100644 index 0000000..97385d1 --- /dev/null +++ b/sql/temp-mas-for-compare.sql @@ -0,0 +1,845 @@ +-- ============================================================================ +-- MASTER SCHEMA SCRIPT +-- ============================================================================ +-- Purpose: +-- This file contains the master SQL schema for the entire Supabase database. +-- It is designed to be a "one-click" script that can be run in a PostgreSQL +-- database to set up the entire backend from scratch, including: +-- 1. Enabling required Postgres extensions. +-- 2. Creating all tables with relationships and constraints. +-- 3. Seeding essential initial data (categories, master items). +-- 6. Defining database functions for business logic. +-- 7. Setting up triggers for automation (e.g., creating user profiles). +-- +-- Usage: +-- For a fresh setup, run this entire script. To reset the environment, +-- first run 'delete_all_tables.sql.txt' and then run this script. + +-- ============================================================================ +-- PART 1: EXTENSIONS +-- ============================================================================ +-- Enable necessary PostgreSQL extensions. +-- postgis: For storing and querying geographic data (store locations). +-- pg_trgm: For trigram-based fuzzy string matching (improving item searches). +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- For generating UUIDs + +-- ============================================================================ +-- PART 2: TABLES +-- ============================================================================ +-- 1. Users - This replaces the Supabase `auth.users` table. +CREATE TABLE IF NOT EXISTS public.users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT, + refresh_token TEXT, + failed_login_attempts INTEGER DEFAULT 0, + last_failed_login TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.users IS 'Stores user authentication information, replacing Supabase auth.'; +COMMENT ON COLUMN public.users.refresh_token IS 'Stores the long-lived refresh token for re-authentication.'; +COMMENT ON COLUMN public.users.failed_login_attempts IS 'Tracks the number of consecutive failed login attempts.'; +COMMENT ON COLUMN public.users.last_failed_login IS 'Timestamp of the last failed login attempt.'; +-- Add an index on the refresh_token for faster lookups when refreshing tokens. +CREATE INDEX IF NOT EXISTS idx_users_refresh_token ON public.users(refresh_token); + +-- 2. Log key user activities for analytics. +-- This needs to be created early as many triggers will insert into it. +CREATE TABLE IF NOT EXISTS public.activity_log ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + action TEXT NOT NULL, + display_text TEXT NOT NULL, + icon TEXT, + details JSONB, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.activity_log IS 'Logs key user and system actions for auditing and display in an activity feed.'; +CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log(user_id); + +-- 3. for public user profiles. +-- This table is linked to the users table and stores non-sensitive user data. +CREATE TABLE IF NOT EXISTS public.profiles ( + id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE, + full_name TEXT, + avatar_url TEXT, + preferences JSONB, + role TEXT CHECK (role IN ('admin', 'user')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + created_by UUID REFERENCES public.users(id) ON DELETE SET NULL, + updated_by UUID REFERENCES public.users(id) ON DELETE SET NULL +); +COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.'; + +-- 4. The 'stores' table for normalized store data. +CREATE TABLE IF NOT EXISTS public.stores ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + logo_url TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + created_by UUID REFERENCES public.users(id) ON DELETE SET NULL +); +COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).'; + +-- 5. The 'categories' table for normalized category data. +CREATE TABLE IF NOT EXISTS public.categories ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.categories IS 'Stores a predefined list of grocery item categories (e.g., ''Fruits & Vegetables'', ''Dairy & Eggs'').'; + +-- 6. flyers' table +CREATE TABLE IF NOT EXISTS public.flyers ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + file_name TEXT NOT NULL, + image_url TEXT NOT NULL, + checksum TEXT UNIQUE, + store_id BIGINT REFERENCES public.stores(id), + valid_from DATE, + valid_to DATE, + store_address TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.'; +CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id); +COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").'; +COMMENT ON COLUMN public.flyers.image_url IS 'The public URL of the primary flyer image stored in Supabase Storage.'; +COMMENT ON COLUMN public.flyers.checksum IS 'A SHA-256 hash of the original file content to prevent duplicate processing.'; +COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a specific store in the `stores` table.'; +COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.'; +COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.'; +COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.'; + +-- 7. The 'master_grocery_items' table. This is the master dictionary. +CREATE TABLE IF NOT EXISTS public.master_grocery_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + category_id BIGINT REFERENCES public.categories(id), + is_allergen BOOLEAN DEFAULT false, + allergy_info JSONB, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + created_by UUID REFERENCES public.users(id) ON DELETE SET NULL +); +COMMENT ON TABLE public.master_grocery_items IS 'The master dictionary of canonical grocery items. Each item has a unique name and is linked to a category.'; +CREATE INDEX IF NOT EXISTS idx_master_grocery_items_category_id ON public.master_grocery_items(category_id); + +-- 8. The 'user_watched_items' table. This links to the master list. +CREATE TABLE IF NOT EXISTS public.user_watched_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(user_id, master_item_id) +); +COMMENT ON TABLE public.user_watched_items IS 'A linking table that represents a user''s personal watchlist of grocery items.'; +CREATE INDEX IF NOT EXISTS idx_user_watched_items_master_item_id ON public.user_watched_items(master_item_id); + +-- 9. The 'flyer_items' table. This stores individual items from flyers. +CREATE TABLE IF NOT EXISTS public.flyer_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + flyer_id BIGINT REFERENCES public.flyers(id) ON DELETE CASCADE, + item TEXT NOT NULL, + price_display TEXT NOT NULL, + price_in_cents INTEGER, + quantity_num NUMERIC, + quantity TEXT NOT NULL, + category_id BIGINT REFERENCES public.categories(id), + category_name TEXT, + unit_price JSONB, + view_count INTEGER DEFAULT 0 NOT NULL, + click_count INTEGER DEFAULT 0 NOT NULL, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + product_id BIGINT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.flyer_items IS 'Stores individual items extracted from a specific flyer.'; +COMMENT ON COLUMN public.flyer_items.flyer_id IS 'Foreign key linking this item to its parent flyer in the `flyers` table.'; +COMMENT ON COLUMN public.flyer_items.item IS 'The raw item name as it appears in the flyer (e.g., "Granny Smith Apples").'; +COMMENT ON COLUMN public.flyer_items.price_display IS 'The raw price string from the flyer (e.g., "$3.99", "2 for $5.00").'; +COMMENT ON COLUMN public.flyer_items.price_in_cents IS 'The normalized price for a single item, in cents, for easier sorting and comparison.'; +COMMENT ON COLUMN public.flyer_items.quantity IS 'The raw quantity or deal description string (e.g., "per lb", "500g bag").'; +COMMENT ON COLUMN public.flyer_items.quantity_num IS 'The parsed primary numeric value from the quantity string (e.g., 500 from "500g").'; +COMMENT ON COLUMN public.flyer_items.master_item_id IS 'Foreign key linking this flyer item to its canonical entry in `master_grocery_items`. Null if no match was found.'; +COMMENT ON COLUMN public.flyer_items.category_id IS 'Foreign key to the item''s category. Can be redundant if master_item_id is set, but useful.'; +COMMENT ON COLUMN public.flyer_items.category_name IS 'The denormalized name of the category for faster UI display without extra joins.'; +COMMENT ON COLUMN public.flyer_items.unit_price IS 'A JSONB object storing the calculated unit price (e.g., {"value": 1.99, "unit": "lb"}) for standardized price comparisons.'; +COMMENT ON COLUMN public.flyer_items.product_id IS 'A foreign key for future use, to link to a specific product with a UPC code in the `products` table.'; +CREATE INDEX IF NOT EXISTS idx_flyer_items_flyer_id ON public.flyer_items(flyer_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_master_item_id ON public.flyer_items(master_item_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_category_id ON public.flyer_items(category_id); +CREATE INDEX IF NOT EXISTS idx_flyer_items_product_id ON public.flyer_items(product_id); +-- Add a GIN index to the 'item' column for fast fuzzy text searching. +-- This requires the pg_trgm extension. +CREATE INDEX IF NOT EXISTS flyer_items_item_trgm_idx ON public.flyer_items USING GIN (item gin_trgm_ops); + +-- 10. For user-defined alerts on watched items. +CREATE TABLE IF NOT EXISTS public.user_alerts ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_watched_item_id BIGINT NOT NULL REFERENCES public.user_watched_items(id) ON DELETE CASCADE, + alert_type TEXT NOT NULL CHECK (alert_type IN ('PRICE_BELOW', 'PERCENT_OFF_AVERAGE')), + threshold_value NUMERIC NOT NULL, + is_active BOOLEAN DEFAULT true NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_alerts IS 'Stores user-configured alert rules for their watched items.'; +COMMENT ON COLUMN public.user_alerts.alert_type IS 'The condition that triggers the alert, e.g., ''PRICE_BELOW''.'; +COMMENT ON COLUMN public.user_alerts.threshold_value IS 'The numeric threshold for the alert condition (e.g., price in cents, or percentage).'; +CREATE INDEX IF NOT EXISTS idx_user_alerts_user_watched_item_id ON public.user_alerts(user_watched_item_id); + +-- 11. Store notifications for users. +CREATE TABLE IF NOT EXISTS public.notifications ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + link_url TEXT, + is_read BOOLEAN DEFAULT false NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.notifications IS 'A central log of notifications generated for users, such as price alerts.'; +COMMENT ON COLUMN public.notifications.content IS 'The notification message displayed to the user.'; +COMMENT ON COLUMN public.notifications.link_url IS 'A URL to navigate to when the notification is clicked.'; +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON public.notifications(user_id); + +-- 12. Store individual store locations with geographic data. +CREATE TABLE IF NOT EXISTS public.store_locations ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, + address TEXT NOT NULL, + city TEXT, + province_state TEXT, + postal_code TEXT, + location GEOGRAPHY(Point, 4326), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.store_locations IS 'Stores physical locations of stores with geographic data for proximity searches.'; +COMMENT ON COLUMN public.store_locations.location IS 'Geographic coordinates (longitude, latitude) of the store.'; +CREATE INDEX IF NOT EXISTS idx_store_locations_store_id ON public.store_locations(store_id); +-- Add a GIST index for efficient geographic queries. +-- This requires the postgis extension. +CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location); + +-- 13. For aggregated, historical price data for master items. +CREATE TABLE IF NOT EXISTS public.item_price_history ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + summary_date DATE NOT NULL, + store_location_id BIGINT REFERENCES public.store_locations(id) ON DELETE CASCADE, + min_price_in_cents INTEGER, + max_price_in_cents INTEGER, + avg_price_in_cents INTEGER, + data_points_count INTEGER DEFAULT 0 NOT NULL, + UNIQUE(master_item_id, summary_date, store_location_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.item_price_history IS 'Serves as a summary table to speed up charting and analytics.'; +COMMENT ON COLUMN public.item_price_history.summary_date IS 'The date for which the price data is summarized.'; +COMMENT ON COLUMN public.item_price_history.min_price_in_cents IS 'The lowest price found for this item on this day, in cents,'; +COMMENT ON COLUMN public.item_price_history.max_price_in_cents IS 'The highest price found for this item on this day, in cents.'; +COMMENT ON COLUMN public.item_price_history.avg_price_in_cents IS 'The average price found for this item on this day, in cents.'; +COMMENT ON COLUMN public.item_price_history.data_points_count IS 'How many data points were used for this summary.'; +CREATE INDEX IF NOT EXISTS idx_item_price_history_master_item_id ON public.item_price_history(master_item_id); +CREATE INDEX IF NOT EXISTS idx_item_price_history_store_location_id ON public.item_price_history(store_location_id); + +-- 14. Map various names to a single master grocery item. +CREATE TABLE IF NOT EXISTS public.master_item_aliases ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + alias TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.master_item_aliases IS 'Stores synonyms or alternative names for master items to improve matching.'; +COMMENT ON COLUMN public.master_item_aliases.alias IS 'An alternative name, e.g., "Ground Chuck" for the master item "Ground Beef".'; +CREATE INDEX IF NOT EXISTS idx_master_item_aliases_master_item_id ON public.master_item_aliases(master_item_id); + +-- 15. For user shopping lists. +CREATE TABLE IF NOT EXISTS public.shopping_lists ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.shopping_lists IS 'Stores user-created shopping lists, e.g., "Weekly Groceries".'; +CREATE INDEX IF NOT EXISTS idx_shopping_lists_user_id ON public.shopping_lists(user_id); + +-- 16. For items in a user's shopping list. +CREATE TABLE IF NOT EXISTS public.shopping_list_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + custom_item_name TEXT, + quantity NUMERIC DEFAULT 1 NOT NULL, + is_purchased BOOLEAN DEFAULT false NOT NULL, + notes TEXT, + added_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) +); +COMMENT ON TABLE public.shopping_list_items IS 'Contains individual items for a specific shopping list.'; +COMMENT ON COLUMN public.shopping_list_items.custom_item_name IS 'For items not in the master list, e.g., "Grandma''s special spice mix".'; +COMMENT ON COLUMN public.shopping_list_items.is_purchased IS 'Lets users check items off their list as they shop.'; +CREATE INDEX IF NOT EXISTS idx_shopping_list_items_shopping_list_id ON public.shopping_list_items(shopping_list_id); +CREATE INDEX IF NOT EXISTS idx_shopping_list_items_master_item_id ON public.shopping_list_items(master_item_id); + +-- 17. Manage shared access to shopping lists. +CREATE TABLE IF NOT EXISTS public.shared_shopping_lists ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, + shared_by_user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + shared_with_user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + permission_level TEXT NOT NULL CHECK (permission_level IN ('view', 'edit')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(shopping_list_id, shared_with_user_id) +); +COMMENT ON TABLE public.shared_shopping_lists IS 'Allows users to share shopping lists with others and set permissions.'; +CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shopping_list_id ON public.shared_shopping_lists(shopping_list_id); +CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_by_user_id ON public.shared_shopping_lists(shared_by_user_id); +CREATE INDEX IF NOT EXISTS idx_shared_shopping_lists_shared_with_user_id ON public.shared_shopping_lists(shared_with_user_id); + +-- 18. Store a user's collection of planned meals for a date range. +CREATE TABLE IF NOT EXISTS public.menu_plans ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT date_range_check CHECK (end_date >= start_date) +); +COMMENT ON TABLE public.menu_plans IS 'Represents a user''s meal plan for a specific period, e.g., "Week of Oct 23".'; +CREATE INDEX IF NOT EXISTS idx_menu_plans_user_id ON public.menu_plans(user_id); + +-- 19. Manage shared access to menu plans. +CREATE TABLE IF NOT EXISTS public.shared_menu_plans ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, + shared_by_user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + shared_with_user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + permission_level TEXT NOT NULL CHECK (permission_level IN ('view', 'edit')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(menu_plan_id, shared_with_user_id) +); +COMMENT ON TABLE public.shared_menu_plans IS 'Allows users to share and collaborate on meal plans.'; +CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_menu_plan_id ON public.shared_menu_plans(menu_plan_id); +CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_by_user_id ON public.shared_menu_plans(shared_by_user_id); +CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_with_user_id ON public.shared_menu_plans(shared_with_user_id); + +-- 20. Store user-submitted corrections for flyer items. +CREATE TABLE IF NOT EXISTS public.suggested_corrections ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.users(id), + correction_type TEXT NOT NULL, + suggested_value TEXT NOT NULL, + status TEXT DEFAULT 'pending' NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + reviewed_notes TEXT, + reviewed_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.suggested_corrections IS 'A queue for user-submitted data corrections, enabling crowdsourced data quality improvements.'; +COMMENT ON COLUMN public.suggested_corrections.correction_type IS 'The type of error the user is reporting.'; +COMMENT ON COLUMN public.suggested_corrections.suggested_value IS 'The corrected value proposed by the user (e.g., a new price or master_item_id).'; +COMMENT ON COLUMN public.suggested_corrections.status IS 'The moderation status of the correction: pending, approved, or rejected.'; +CREATE INDEX IF NOT EXISTS idx_suggested_corrections_flyer_item_id ON public.suggested_corrections(flyer_item_id); +CREATE INDEX IF NOT EXISTS idx_suggested_corrections_user_id ON public.suggested_corrections(user_id); + +-- 21. For prices submitted directly by users from in-store. +CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id), + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id), + store_id BIGINT NOT NULL REFERENCES public.stores(id), + price_in_cents INTEGER NOT NULL, + photo_url TEXT, + upvotes INTEGER DEFAULT 0 NOT NULL, + downvotes INTEGER DEFAULT 0 NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.'; +COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.'; +COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.'; +CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_user_id ON public.user_submitted_prices(user_id); +CREATE INDEX IF NOT EXISTS idx_user_submitted_prices_master_item_id ON public.user_submitted_prices(master_item_id); + +-- 22. Log flyer items that could not be automatically matched to a master item. +CREATE TABLE IF NOT EXISTS public.unmatched_flyer_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(id) ON DELETE CASCADE, + status TEXT DEFAULT 'pending' NOT NULL CHECK (status IN ('pending', 'reviewed', 'ignored')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + reviewed_at TIMESTAMPTZ, + UNIQUE(flyer_item_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.unmatched_flyer_items IS 'A queue for reviewing flyer items that the system failed to automatically match.'; +CREATE INDEX IF NOT EXISTS idx_unmatched_flyer_items_flyer_item_id ON public.unmatched_flyer_items(flyer_item_id); + +-- 23. Store brand information. +CREATE TABLE IF NOT EXISTS public.brands ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + logo_url TEXT, + store_id BIGINT REFERENCES public.stores(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".'; +COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.'; + +-- 24. For specific products, linking a master item with a brand and size. +CREATE TABLE IF NOT EXISTS public.products ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id), + brand_id BIGINT REFERENCES public.brands(id), + name TEXT NOT NULL, + description TEXT, + size TEXT, + upc_code TEXT UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.products IS 'Represents a specific, sellable product, combining a generic item with a brand and size.'; +COMMENT ON COLUMN public.products.upc_code IS 'Universal Product Code, if available, for exact product matching.'; +COMMENT ON COLUMN public.products.brand_id IS 'Can be null for generic/store-brand items.'; +COMMENT ON COLUMN public.products.name IS 'Prime Raised without Antibiotics Chicken Breast.'; +COMMENT ON COLUMN public.products.size IS 'e.g., "4L", "500g".'; +CREATE INDEX IF NOT EXISTS idx_products_master_item_id ON public.products(master_item_id); +CREATE INDEX IF NOT EXISTS idx_products_brand_id ON public.products(brand_id); + +-- 25. Linking table for when one flyer is valid for multiple locations. +CREATE TABLE IF NOT EXISTS public.flyer_locations ( + flyer_id BIGINT NOT NULL REFERENCES public.flyers(id) ON DELETE CASCADE, + store_location_id BIGINT NOT NULL REFERENCES public.store_locations(id) ON DELETE CASCADE, + PRIMARY KEY (flyer_id, store_location_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.flyer_locations IS 'A linking table associating a single flyer with multiple store locations where its deals are valid.'; +CREATE INDEX IF NOT EXISTS idx_flyer_locations_flyer_id ON public.flyer_locations(flyer_id); +CREATE INDEX IF NOT EXISTS idx_flyer_locations_store_location_id ON public.flyer_locations(store_location_id); + +-- 26. Store recipes, which can be user-created or pre-populated. +CREATE TABLE IF NOT EXISTS public.recipes ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE CASCADE, + original_recipe_id BIGINT REFERENCES public.recipes(id) ON DELETE SET NULL, + name TEXT NOT NULL, + description TEXT, + instructions TEXT, + prep_time_minutes INTEGER, + cook_time_minutes INTEGER, + servings INTEGER, + photo_url TEXT, + calories_per_serving INTEGER, + protein_grams NUMERIC, + fat_grams NUMERIC, + carb_grams NUMERIC, + avg_rating NUMERIC(2,1) DEFAULT 0.0, + status TEXT DEFAULT 'private' NOT NULL CHECK (status IN ('private', 'pending_review', 'public')), + rating_count INTEGER DEFAULT 0 NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.'; +COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.'; +COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.'; +COMMENT ON COLUMN public.recipes.user_id IS 'Can be a system recipe (user_id is NULL) or user-submitted.'; +COMMENT ON COLUMN public.recipes.avg_rating IS 'Aggregated rating data for fast sorting/display.'; +COMMENT ON COLUMN public.recipes.calories_per_serving IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.protein_grams IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.fat_grams IS 'Optional nutritional information.'; +COMMENT ON COLUMN public.recipes.carb_grams IS 'Optional nutritional information.'; +CREATE INDEX IF NOT EXISTS idx_recipes_user_id ON public.recipes(user_id); +CREATE INDEX IF NOT EXISTS idx_recipes_original_recipe_id ON public.recipes(original_recipe_id); + +-- 27. For ingredients required for each recipe. +CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id), + quantity NUMERIC NOT NULL, + unit TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_ingredients IS 'Defines the ingredients and quantities needed for a recipe.'; +COMMENT ON COLUMN public.recipe_ingredients.unit IS 'e.g., "cups", "tbsp", "g", "each".'; +CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_recipe_id ON public.recipe_ingredients(recipe_id); +CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_master_item_id ON public.recipe_ingredients(master_item_id); + +-- 28. Suggest ingredient substitutions for a recipe. +CREATE TABLE IF NOT EXISTS public.recipe_ingredient_substitutions ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + recipe_ingredient_id BIGINT NOT NULL REFERENCES public.recipe_ingredients(id) ON DELETE CASCADE, + substitute_master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + notes TEXT, + UNIQUE(recipe_ingredient_id, substitute_master_item_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_ingredient_substitutions IS 'Stores suggested alternative ingredients for a recipe item (e.g., "butter" for "olive oil").'; +CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_recipe_ingredient_id ON public.recipe_ingredient_substitutions(recipe_ingredient_id); +CREATE INDEX IF NOT EXISTS idx_recipe_ingredient_substitutions_substitute_master_item_id ON public.recipe_ingredient_substitutions(substitute_master_item_id); + +-- 29. Store a predefined list of tags for recipes. +CREATE TABLE IF NOT EXISTS public.tags ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.tags IS 'Stores tags for categorizing recipes, e.g., "Vegetarian", "Quick & Easy".'; + +-- 30. Associate multiple tags with a recipe. +CREATE TABLE IF NOT EXISTS public.recipe_tags ( + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + tag_id BIGINT NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE, + PRIMARY KEY (recipe_id, tag_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_tags IS 'A linking table to associate multiple tags with a single recipe.'; +CREATE INDEX IF NOT EXISTS idx_recipe_tags_recipe_id ON public.recipe_tags(recipe_id); +CREATE INDEX IF NOT EXISTS idx_recipe_tags_tag_id ON public.recipe_tags(tag_id); + +-- 31. Store a predefined list of kitchen appliances. +CREATE TABLE IF NOT EXISTS public.appliances ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.appliances IS 'A predefined list of kitchen appliances (e.g., Air Fryer, Instant Pot).'; + +-- 32. Associate recipes with required appliances. +CREATE TABLE IF NOT EXISTS public.recipe_appliances ( + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, + PRIMARY KEY (recipe_id, appliance_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_appliances IS 'Links recipes to the specific kitchen appliances they require.'; +CREATE INDEX IF NOT EXISTS idx_recipe_appliances_recipe_id ON public.recipe_appliances(recipe_id); +CREATE INDEX IF NOT EXISTS idx_recipe_appliances_appliance_id ON public.recipe_appliances(appliance_id); + +-- 33. Store individual user ratings for recipes. +CREATE TABLE IF NOT EXISTS public.recipe_ratings ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), + comment TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(recipe_id, user_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_ratings IS 'Stores individual user ratings for recipes, ensuring a user can only rate a recipe once.'; +CREATE INDEX IF NOT EXISTS idx_recipe_ratings_recipe_id ON public.recipe_ratings(recipe_id); +CREATE INDEX IF NOT EXISTS idx_recipe_ratings_user_id ON public.recipe_ratings(user_id); + +-- 34. For user comments on recipes to enable discussion. +CREATE TABLE IF NOT EXISTS public.recipe_comments ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + parent_comment_id BIGINT REFERENCES public.recipe_comments(id) ON DELETE CASCADE, + content TEXT NOT NULL, + status TEXT DEFAULT 'visible' NOT NULL CHECK (status IN ('visible', 'hidden', 'reported')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_comments IS 'Allows for threaded discussions and comments on recipes.'; +COMMENT ON COLUMN public.recipe_comments.parent_comment_id IS 'For threaded comments.'; +CREATE INDEX IF NOT EXISTS idx_recipe_comments_recipe_id ON public.recipe_comments(recipe_id); +CREATE INDEX IF NOT EXISTS idx_recipe_comments_user_id ON public.recipe_comments(user_id); +CREATE INDEX IF NOT EXISTS idx_recipe_comments_parent_comment_id ON public.recipe_comments(parent_comment_id); + +-- 35. For users to define locations within their pantry. +CREATE TABLE IF NOT EXISTS public.pantry_locations ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(user_id, name) +); +COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").'; +CREATE INDEX IF NOT EXISTS idx_pantry_locations_user_id ON public.pantry_locations(user_id); + +-- 36. Associate a recipe with a specific date and meal type within a menu plan. +CREATE TABLE IF NOT EXISTS public.planned_meals ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + menu_plan_id BIGINT NOT NULL REFERENCES public.menu_plans(id) ON DELETE CASCADE, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + plan_date DATE NOT NULL, + meal_type TEXT NOT NULL, + servings_to_cook INTEGER, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.planned_meals IS 'Assigns a recipe to a specific day and meal type within a user''s menu plan.'; +COMMENT ON COLUMN public.planned_meals.meal_type IS 'The designated meal for the recipe, e.g., ''Breakfast'', ''Lunch'', ''Dinner''.'; +CREATE INDEX IF NOT EXISTS idx_planned_meals_menu_plan_id ON public.planned_meals(menu_plan_id); +CREATE INDEX IF NOT EXISTS idx_planned_meals_recipe_id ON public.planned_meals(recipe_id); + +-- 37. Track the grocery items a user currently has in their pantry. +CREATE TABLE IF NOT EXISTS public.pantry_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + quantity NUMERIC NOT NULL, + unit TEXT, + best_before_date DATE, + pantry_location_id BIGINT REFERENCES public.pantry_locations(id) ON DELETE SET NULL, + notification_sent_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + UNIQUE(user_id, master_item_id, unit) +); +COMMENT ON TABLE public.pantry_items IS 'Tracks a user''s personal inventory of grocery items to enable smart shopping lists.'; +COMMENT ON COLUMN public.pantry_items.quantity IS 'The current amount of the item. Convention: use grams for weight, mL for volume where applicable.'; +COMMENT ON COLUMN public.pantry_items.pantry_location_id IS 'Links the item to a user-defined location like "Fridge" or "Freezer".'; +COMMENT ON COLUMN public.pantry_items.unit IS 'e.g., ''g'', ''ml'', ''items''. Should align with recipe_ingredients.unit and quantity convention.'; +CREATE INDEX IF NOT EXISTS idx_pantry_items_user_id ON public.pantry_items(user_id); +CREATE INDEX IF NOT EXISTS idx_pantry_items_master_item_id ON public.pantry_items(master_item_id); +CREATE INDEX IF NOT EXISTS idx_pantry_items_pantry_location_id ON public.pantry_items(pantry_location_id); + +-- 38. Store password reset tokens. +CREATE TABLE IF NOT EXISTS public.password_reset_tokens ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.password_reset_tokens IS 'Stores secure, single-use tokens for password reset requests.'; +COMMENT ON COLUMN public.password_reset_tokens.token_hash IS 'A bcrypt hash of the reset token sent to the user.'; +COMMENT ON COLUMN public.password_reset_tokens.expires_at IS 'The timestamp when this token is no longer valid.'; +CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON public.password_reset_tokens(user_id); +CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash); + +-- 39. Store unit conversion factors for specific master grocery items. +CREATE TABLE IF NOT EXISTS public.unit_conversions ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + from_unit TEXT NOT NULL, + to_unit TEXT NOT NULL, + factor NUMERIC NOT NULL, + UNIQUE(master_item_id, from_unit, to_unit), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.unit_conversions IS 'Stores item-specific unit conversion factors (e.g., grams of flour to cups).'; +COMMENT ON COLUMN public.unit_conversions.factor IS 'The multiplication factor to convert from_unit to to_unit.'; +CREATE INDEX IF NOT EXISTS idx_unit_conversions_master_item_id ON public.unit_conversions(master_item_id); + +-- 40. For users to create their own private aliases for items. +CREATE TABLE IF NOT EXISTS public.user_item_aliases ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(id) ON DELETE CASCADE, + alias TEXT NOT NULL, + UNIQUE(user_id, alias), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_item_aliases IS 'Allows users to create personal aliases for grocery items (e.g., "Dad''s Cereal").'; +CREATE INDEX IF NOT EXISTS idx_user_item_aliases_user_id ON public.user_item_aliases(user_id); +CREATE INDEX IF NOT EXISTS idx_user_item_aliases_master_item_id ON public.user_item_aliases(master_item_id); + +-- 41. For users to mark their favorite recipes. +CREATE TABLE IF NOT EXISTS public.favorite_recipes ( + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + PRIMARY KEY (user_id, recipe_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.favorite_recipes IS 'A simple linking table for users to mark their favorite recipes.'; +CREATE INDEX IF NOT EXISTS idx_favorite_recipes_user_id ON public.favorite_recipes(user_id); +CREATE INDEX IF NOT EXISTS idx_favorite_recipes_recipe_id ON public.favorite_recipes(recipe_id); + +-- 42. For users to mark their favorite stores. +CREATE TABLE IF NOT EXISTS public.favorite_stores ( + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + store_id BIGINT NOT NULL REFERENCES public.stores(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + PRIMARY KEY (user_id, store_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.favorite_stores IS 'A simple linking table for users to mark their favorite stores.'; +CREATE INDEX IF NOT EXISTS idx_favorite_stores_user_id ON public.favorite_stores(user_id); +CREATE INDEX IF NOT EXISTS idx_favorite_stores_store_id ON public.favorite_stores(store_id); + +-- 43. For users to group recipes into collections. +CREATE TABLE IF NOT EXISTS public.recipe_collections ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_collections IS 'Allows users to create personal collections of recipes (e.g., "Holiday Baking").'; +CREATE INDEX IF NOT EXISTS idx_recipe_collections_user_id ON public.recipe_collections(user_id); + +-- 44. Associate recipes with a user's collection. +CREATE TABLE IF NOT EXISTS public.recipe_collection_items ( + collection_id BIGINT NOT NULL REFERENCES public.recipe_collections(id) ON DELETE CASCADE, + recipe_id BIGINT NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE, + added_at TIMESTAMPTZ DEFAULT now() NOT NULL, + PRIMARY KEY (collection_id, recipe_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.recipe_collection_items IS 'Links recipes to a user-defined collection.'; +CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_collection_id ON public.recipe_collection_items(collection_id); +CREATE INDEX IF NOT EXISTS idx_recipe_collection_items_recipe_id ON public.recipe_collection_items(recipe_id); + +-- 45. Log user search queries for analysis. +CREATE TABLE IF NOT EXISTS public.search_queries ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, + query_text TEXT NOT NULL, + result_count INTEGER, + was_successful BOOLEAN, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.search_queries IS 'Logs user search queries to analyze search effectiveness and identify gaps in data.'; +COMMENT ON COLUMN public.search_queries.was_successful IS 'Indicates if the user interacted with a search result.'; +CREATE INDEX IF NOT EXISTS idx_search_queries_user_id ON public.search_queries(user_id); + +-- 46. Store historical records of completed shopping trips. +CREATE TABLE IF NOT EXISTS public.shopping_trips ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + shopping_list_id BIGINT REFERENCES public.shopping_lists(id) ON DELETE SET NULL, + completed_at TIMESTAMPTZ DEFAULT now() NOT NULL, + total_spent_cents INTEGER, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.shopping_trips IS 'A historical record of a completed shopping trip.'; +COMMENT ON COLUMN public.shopping_trips.total_spent_cents IS 'The total amount spent on this shopping trip, if provided by the user.'; +CREATE INDEX IF NOT EXISTS idx_shopping_trips_user_id ON public.shopping_trips(user_id); +CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shopping_trips(shopping_list_id); + +-- 47. Store the items purchased during a specific shopping trip. +CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(id) ON DELETE CASCADE, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + custom_item_name TEXT, + quantity NUMERIC NOT NULL, + price_paid_cents INTEGER, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT trip_must_have_item_identifier CHECK (master_item_id IS NOT NULL OR custom_item_name IS NOT NULL) +); +COMMENT ON TABLE public.shopping_trip_items IS 'A historical log of items purchased during a shopping trip.'; +COMMENT ON COLUMN public.shopping_trip_items.price_paid_cents IS 'The actual price paid for the item during the trip, if provided.'; +CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_shopping_trip_id ON public.shopping_trip_items(shopping_trip_id); +CREATE INDEX IF NOT EXISTS idx_shopping_trip_items_master_item_id ON public.shopping_trip_items(master_item_id); + +-- 48. Store predefined dietary restrictions (diets and allergies). +CREATE TABLE IF NOT EXISTS public.dietary_restrictions ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL UNIQUE, + type TEXT NOT NULL CHECK (type IN ('diet', 'allergy')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.dietary_restrictions IS 'A predefined list of common diets (e.g., Vegan) and allergies (e.g., Nut Allergy).'; + +-- 49. For a user's specific dietary restrictions. +CREATE TABLE IF NOT EXISTS public.user_dietary_restrictions ( + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + restriction_id BIGINT NOT NULL REFERENCES public.dietary_restrictions(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, restriction_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_dietary_restrictions IS 'Connects users to their selected dietary needs and allergies.'; +CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_user_id ON public.user_dietary_restrictions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_dietary_restrictions_restriction_id ON public.user_dietary_restrictions(restriction_id); + +-- 50. For a user's owned kitchen appliances. +CREATE TABLE IF NOT EXISTS public.user_appliances ( + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + appliance_id BIGINT NOT NULL REFERENCES public.appliances(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, appliance_id), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user owns to help with recipe recommendations.'; +CREATE INDEX IF NOT EXISTS idx_user_appliances_user_id ON public.user_appliances(user_id); +CREATE INDEX IF NOT EXISTS idx_user_appliances_appliance_id ON public.user_appliances(appliance_id); + +-- 51. Manage the social graph (following relationships). +CREATE TABLE IF NOT EXISTS public.user_follows ( + follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + following_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + PRIMARY KEY (follower_id, following_id), + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + CONSTRAINT cant_follow_self CHECK (follower_id <> following_id) +); +COMMENT ON TABLE public.user_follows IS 'Stores user following relationships to build a social graph.'; +CREATE INDEX IF NOT EXISTS idx_user_follows_follower_id ON public.user_follows(follower_id); +CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(following_id); + +-- 52. Store uploaded user receipts for purchase tracking and analysis. +CREATE TABLE IF NOT EXISTS public.receipts ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + store_id BIGINT REFERENCES public.stores(id), + receipt_image_url TEXT NOT NULL, + transaction_date TIMESTAMPTZ, + total_amount_cents INTEGER, + status TEXT DEFAULT 'pending' NOT NULL CHECK (status IN ('pending', 'processing', 'completed', 'failed')), + raw_text TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + processed_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.'; +CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); +CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); + +-- 53. Store individual line items extracted from a user receipt. +CREATE TABLE IF NOT EXISTS public.receipt_items ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + receipt_id BIGINT NOT NULL REFERENCES public.receipts(id) ON DELETE CASCADE, + raw_item_description TEXT NOT NULL, + quantity NUMERIC DEFAULT 1 NOT NULL, + price_paid_cents INTEGER NOT NULL, + master_item_id BIGINT REFERENCES public.master_grocery_items(id), + product_id BIGINT REFERENCES public.products(id), + status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')), + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL +); +COMMENT ON TABLE public.receipt_items IS 'Stores individual line items extracted from a user receipt.'; +CREATE INDEX IF NOT EXISTS idx_receipt_items_receipt_id ON public.receipt_items(receipt_id); +CREATE INDEX IF NOT EXISTS idx_receipt_items_master_item_id ON public.receipt_items(master_item_id); diff --git a/sql/verify_rollup.ps1 b/sql/verify_rollup.ps1 new file mode 100644 index 0000000..dee544d --- /dev/null +++ b/sql/verify_rollup.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + SQL ROLLUP VERIFICATION SCRIPT (POWERSHELL) +.DESCRIPTION + This script verifies that the 'master_schema_rollup.sql' file is an exact + concatenation of the individual SQL component files. This helps ensure + that all database setup scripts are synchronized. + + The script will exit with code 0 if they match, and 1 if they don't. +.EXAMPLE + From the root of the project, run: + .\sql\verify_rollup.ps1 +#> + +# Set the script to stop on errors +$ErrorActionPreference = "Stop" + +# Define file paths relative to the script's location +$PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition +$ProjectRoot = Resolve-Path -Path (Join-Path $PSScriptRoot "..") +$MasterFile = Join-Path $ProjectRoot "sql\master_schema_rollup.sql" + +# The individual files to concatenate, IN ORDER. +$SourceFiles = @( + (Join-Path $ProjectRoot "sql\initial_schema.sql"), + (Join-Path $ProjectRoot "sql\initial_data.sql"), + (Join-Path $ProjectRoot "sql\initial_triggers_and_functions.sql") +) + +Write-Host "Comparing concatenated content with '$MasterFile'..." + +# Compare the master file with the concatenated content of the source files. +$Difference = Compare-Object -ReferenceObject (Get-Content $MasterFile) -DifferenceObject (Get-Content $SourceFiles) + +if ($null -eq $Difference) { + Write-Host "✅ Success: '$MasterFile' is up to date with the individual SQL files." -ForegroundColor Green + exit 0 +} else { + Write-Host "❌ Error: '$MasterFile' is out of sync. Differences found." -ForegroundColor Red + Write-Host "--- $MasterFile" + Write-Host "+++ Concatenated Source Files" + + # Format the output to look like a diff + foreach ($diffLine in $Difference) { + if ($diffLine.SideIndicator -eq "<=") { + Write-Host ("- " + $diffLine.InputObject) -ForegroundColor Red + } elseif ($diffLine.SideIndicator -eq "=>") { + Write-Host ("+ " + $diffLine.InputObject) -ForegroundColor Green + } + } + exit 1 +} \ No newline at end of file diff --git a/sql/verify_rollup.sh b/sql/verify_rollup.sh new file mode 100644 index 0000000..9bbfe97 --- /dev/null +++ b/sql/verify_rollup.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# ============================================================================ +# SQL ROLLUP VERIFICATION SCRIPT +# ============================================================================ +# Purpose: +# This script verifies that the 'master_schema_rollup.sql' file is an exact +# concatenation of the individual SQL component files. This helps ensure +# that all database setup scripts are synchronized. +# +# Usage: +# From the root of the project, run: +# bash sql/verify_rollup.sh +# +# The script will exit with code 0 if they match, and 1 if they don't. +# ============================================================================ + +# Set the script to exit immediately if a command fails +set -e + +# Define file paths relative to the project root +SQL_DIR="sql" +MASTER_FILE="$SQL_DIR/master_schema_rollup.sql" + +# The individual files to concatenate, IN ORDER. +SOURCE_FILES=( + "$SQL_DIR/initial_schema.sql" + "$SQL_DIR/initial_data.sql" + "$SQL_DIR/initial_triggers_and_functions.sql" +) + +# Create a temporary file to hold the concatenated content +TEMP_FILE=$(mktemp) + +# Ensure the temporary file is removed when the script exits +trap 'rm -f "$TEMP_FILE"' EXIT + +# Concatenate all source files into the temp file +echo "Concatenating source files into a temporary file..." +cat "${SOURCE_FILES[@]}" > "$TEMP_FILE" + +echo "Comparing concatenated content with '$MASTER_FILE'..." + +# Use 'diff' to compare the master file with the temporary concatenated file. +# The '-q' flag makes diff quiet and just exit with a status code. +if diff -q "$MASTER_FILE" "$TEMP_FILE"; then + echo "✅ Success: '$MASTER_FILE' is up to date with the individual SQL files." + exit 0 +else + echo "❌ Error: '$MASTER_FILE' is out of sync. Differences found." + echo "To see the differences, run: diff -u \"$MASTER_FILE\" <(cat ${SOURCE_FILES[@]})" + exit 1 +fi