Files
flyer-crawler.projectium.com/sql/initial.sql

657 lines
33 KiB
SQL

-- DONE
-- ============================================================================
-- PART 0.5: USER AUTHENTICATION TABLE
-- ============================================================================
-- 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,
refresh_token TEXT, -- Stores the long-lived refresh token for re-authentication.
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
);
COMMENT ON TABLE public.users IS 'Stores user authentication information, replacing Supabase auth.';
-- 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);
-- 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,
updated_at TIMESTAMPTZ,
full_name TEXT,
avatar_url TEXT,
preferences JSONB,
role TEXT CHECK (role IN ('admin', 'user'))
);
COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the private auth.users table.';
-- DONE
-- 1. Create the 'stores' table for normalized store data.
CREATE TABLE IF NOT EXISTS public.stores (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
name TEXT NOT NULL UNIQUE,
logo_url TEXT
);
COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).';
-- DONE
-- 2. Create 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
);
COMMENT ON TABLE public.categories IS 'Stores a predefined list of grocery item categories (e.g., ''Fruits & Vegetables'', ''Dairy & Eggs'').';
-- DONE
-- 3. Create the 'flyers' table with its full, final schema.
CREATE TABLE IF NOT EXISTS public.flyers (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
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
);
COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.';
-- DONE
-- 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,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
name TEXT NOT NULL UNIQUE,
category_id BIGINT REFERENCES public.categories(id),
is_allergen BOOLEAN DEFAULT false,
allergy_info JSONB
);
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.';
-- DONE
-- 5. Create 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,
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.';
-- DONE
-- 6. Create the 'flyer_items' table with its full, final schema.
CREATE TABLE IF NOT EXISTS public.flyer_items (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
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, -- Denormalized for easier display
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 -- Future use for specific product linking
);
COMMENT ON TABLE public.flyer_items IS 'Stores individual items extracted from a specific flyer.';
-- DONE
-- 7. Create a table 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
);
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).';
-- DONE
-- 8. Create a table to 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
);
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.';
-- DONE
-- 9. Create a table 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)
);
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.';
-- DONE
-- 10. Create a table to 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
);
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".';
-- DONE
-- 11. Create tables 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
);
COMMENT ON TABLE public.shopping_lists IS 'Stores user-created shopping lists, e.g., "Weekly Groceries".';
-- DONE
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,
-- Ensure one of the item identifiers is present
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.';
-- A table to 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,
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.';
-- A table to 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,
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.';
-- DONE
-- 12. Create a table to 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
);
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.';
-- DONE
-- 13. Create a table 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
);
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.';
-- A table to 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)
);
COMMENT ON TABLE public.unmatched_flyer_items IS 'A queue for reviewing flyer items that the system failed to automatically match.';
-- 14. Pre-populate categories table from a predefined list.
INSERT INTO public.categories (name) VALUES
('Fruits & Vegetables'), ('Meat & Seafood'), ('Dairy & Eggs'), ('Bakery & Bread'),
('Pantry & Dry Goods'), ('Beverages'), ('Frozen Foods'), ('Snacks'), ('Household & Cleaning'),
('Personal Care & Health'), ('Baby & Child'), ('Pet Supplies'), ('Deli & Prepared Foods'),
('Canned Goods'), ('Condiments & Spices'), ('Breakfast & Cereal'), ('Organic'),
('International Foods'), ('Other/Miscellaneous')
ON CONFLICT (name) DO NOTHING;
-- DONE
-- A table to store brand information.
CREATE TABLE IF NOT EXISTS public.brands (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL UNIQUE
);
COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".';
-- DONE
-- A table 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
);
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".';
-- Then, you would update 'flyer_items' to link to this new table.
ALTER TABLE public.flyer_items
ADD COLUMN IF NOT EXISTS product_id BIGINT REFERENCES public.products(id);
ADD CONSTRAINT flyer_items_product_id_fkey
FOREIGN KEY (product_id) REFERENCES public.products(id);
-- Enable trigram support for fuzzy string matching
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- 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);
-- First, enable the PostGIS extension if you haven't already.
-- In Supabase, you can do this under Database -> Extensions.
-- CREATE EXTENSION IF NOT EXISTS postgis;
-- DONE
-- 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) ON DELETE CASCADE,
address TEXT NOT NULL,
city TEXT,
province_state TEXT,
postal_code TEXT,
-- Use the 'geography' type for lat/lon data.
location GEOGRAPHY(Point, 4326)
);
CREATE INDEX IF NOT EXISTS store_locations_geo_idx ON public.store_locations USING GIST (location);
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.';
-- 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);
-- DONE
-- You might also need a linking table if 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)
);
COMMENT ON TABLE public.flyer_locations IS 'A linking table associating a single flyer with multiple store locations where its deals are valid.';
-- done
-- A table to 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, -- Can be a system recipe (user_id is NULL) or user-submitted
original_recipe_id BIGINT REFERENCES public.recipes(id) ON DELETE SET NULL, -- For forked/variation recipes
name TEXT NOT NULL,
description TEXT,
instructions TEXT,
prep_time_minutes INTEGER,
cook_time_minutes INTEGER,
servings INTEGER,
photo_url TEXT,
-- Optional nutritional information
calories_per_serving INTEGER,
protein_grams NUMERIC,
fat_grams NUMERIC,
carb_grams NUMERIC,
-- Aggregated rating data for fast sorting/display
avg_rating NUMERIC(2,1) DEFAULT 0.0 NOT NULL,
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
);
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.';
-- done
-- A linking table 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
);
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".';
-- A table to 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)
);
COMMENT ON TABLE public.recipe_ingredient_substitutions IS 'Stores suggested alternative ingredients for a recipe item (e.g., "butter" for "olive oil").';
-- done
-- A table to 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
);
COMMENT ON TABLE public.tags IS 'Stores tags for categorizing recipes, e.g., "Vegetarian", "Quick & Easy".';
-- done
-- A linking table to 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)
);
COMMENT ON TABLE public.recipe_tags IS 'A linking table to associate multiple tags with a single recipe.';
-- A linking table to 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)
);
COMMENT ON TABLE public.recipe_appliances IS 'Links recipes to the specific kitchen appliances they require.';
-- done
-- A table to 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) -- A user can only rate a recipe once.
);
COMMENT ON TABLE public.recipe_ratings IS 'Stores individual user ratings for recipes, ensuring a user can only rate a recipe once.';
-- A table 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
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ
);
COMMENT ON TABLE public.recipe_comments IS 'Allows for threaded discussions and comments on recipes.';
-- DONE
-- A table to 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,
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".';
-- DONE
-- A table to 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
);
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''.';
-- DONE
-- A table to 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_password_reset_tokens_token_hash ON public.password_reset_tokens(token_hash);
-- A table to 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
);
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.';
-- A table to 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)
);
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.';
-- A table 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)
);
COMMENT ON TABLE public.user_item_aliases IS 'Allows users to create personal aliases for grocery items (e.g., "Dad''s Cereal").';
-- A table 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)
);
COMMENT ON TABLE public.favorite_recipes IS 'A simple linking table for users to mark their favorite recipes.';
-- A table 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)
);
COMMENT ON TABLE public.favorite_stores IS 'A simple linking table for users to mark their favorite stores.';
-- A generic table to log key user activities for analytics.
CREATE TABLE IF NOT EXISTS public.user_activity_log (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
activity_type TEXT NOT NULL,
entity_id TEXT,
details JSONB,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
);
COMMENT ON TABLE public.user_activity_log IS 'Logs key user actions for analytics and behavior analysis.';
-- A table 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
);
COMMENT ON TABLE public.recipe_collections IS 'Allows users to create personal collections of recipes (e.g., "Holiday Baking").';
-- A linking table to 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)
);
COMMENT ON TABLE public.recipe_collection_items IS 'Links recipes to a user-defined collection.';
-- 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,
UNIQUE(user_id, name)
);
COMMENT ON TABLE public.pantry_locations IS 'User-defined locations for organizing pantry items (e.g., "Fridge", "Freezer", "Spice Rack").';
-- 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
);
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.';
-- 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
);
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,
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 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'))
);
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.
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)
);
COMMENT ON TABLE public.user_dietary_restrictions IS 'Connects users to their selected dietary needs and allergies.';
-- 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
);
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)
);
COMMENT ON TABLE public.user_appliances IS 'Tracks the kitchen appliances a user owns to help with recipe recommendations.';
-- 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),
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.';