Files
Kitchen/supabase/migrations/00001_initial_schema.sql

413 lines
19 KiB
PL/PgSQL

-- Migration 00001: Initial Schema for KitchenStock
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enums
CREATE TYPE household_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
CREATE TYPE aldi_region_type AS ENUM ('NORD', 'SUED', 'UNSPECIFIED');
CREATE TYPE unit_type AS ENUM ('COUNT', 'WEIGHT', 'VOLUME', 'OTHER');
CREATE TYPE stock_movement_reason AS ENUM ('PURCHASE', 'CONSUMED', 'CORRECTION', 'INVENTORY', 'EXPIRED', 'DISCARDED', 'TRANSFER', 'INITIAL');
CREATE TYPE shopping_list_status AS ENUM ('ACTIVE', 'COMPLETED', 'ARCHIVED');
CREATE TYPE price_type AS ENUM ('REGULAR', 'OFFER', 'LOYALTY', 'UNKNOWN');
CREATE TYPE source_kind AS ENUM ('USER_PURCHASE', 'USER_ESTIMATE', 'OFFICIAL_RETAILER', 'OFFICIAL_PROSPECT', 'GEMINI_GROUNDED', 'HISTORICAL_AVERAGE', 'AI_ESTIMATE');
CREATE TYPE confidence_level AS ENUM ('HIGH', 'MEDIUM', 'LOW');
CREATE TYPE budget_period AS ENUM ('WEEKLY', 'MONTHLY', 'CUSTOM');
CREATE TYPE inventory_session_status AS ENUM ('OPEN', 'COMPLETED', 'CANCELLED');
CREATE TYPE diet_type AS ENUM ('OMNIVORE', 'VEGETARIAN', 'VEGAN', 'PESCETARIAN', 'CUSTOM');
CREATE TYPE pending_action_status AS ENUM ('PENDING', 'CONFIRMED', 'REJECTED', 'EXPIRED');
-- Trigger to update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 1. Profiles
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
display_name TEXT NOT NULL,
avatar_path TEXT,
locale TEXT NOT NULL DEFAULT 'de-DE',
timezone TEXT NOT NULL DEFAULT 'Europe/Berlin',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 2. Households
CREATE TABLE IF NOT EXISTS public.households (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
currency TEXT NOT NULL DEFAULT 'EUR',
locale TEXT NOT NULL DEFAULT 'de-DE',
timezone TEXT NOT NULL DEFAULT 'Europe/Berlin',
postal_code TEXT,
city TEXT,
aldi_region aldi_region_type NOT NULL DEFAULT 'UNSPECIFIED',
default_servings INT NOT NULL DEFAULT 2 CHECK (default_servings > 0),
owner_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 3. Household Members
CREATE TABLE IF NOT EXISTS public.household_members (
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
role household_role NOT NULL DEFAULT 'MEMBER',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (household_id, user_id)
);
-- 4. Categories
CREATE TABLE IF NOT EXISTS public.categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 5. Locations (supports nesting)
CREATE TABLE IF NOT EXISTS public.locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
parent_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
name TEXT NOT NULL,
icon TEXT,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 6. Units
CREATE TABLE IF NOT EXISTS public.units (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID REFERENCES public.households(id) ON DELETE CASCADE, -- NULL for system defaults
name TEXT NOT NULL,
abbreviation TEXT NOT NULL,
unit_type unit_type NOT NULL DEFAULT 'COUNT',
allows_decimals BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 7. Products
CREATE TABLE IF NOT EXISTS public.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
category_id UUID REFERENCES public.categories(id) ON DELETE SET NULL,
default_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
default_unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
name TEXT NOT NULL,
brand TEXT,
description TEXT,
barcode TEXT,
image_path TEXT,
package_size NUMERIC(10, 3) NOT NULL DEFAULT 1.0,
package_unit TEXT NOT NULL DEFAULT 'Stück',
minimum_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (minimum_quantity >= 0),
favorite BOOLEAN NOT NULL DEFAULT false,
preferred_store TEXT,
estimated_price_cents INT CHECK (estimated_price_cents >= 0),
price_updated_at TIMESTAMPTZ,
archived BOOLEAN NOT NULL DEFAULT false,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 8. Inventory Batches
CREATE TABLE IF NOT EXISTS public.inventory_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (quantity >= 0),
expiration_date DATE,
purchase_date DATE,
purchase_price_cents INT CHECK (purchase_price_cents >= 0),
store TEXT,
lot_number TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 9. Stock Movements
CREATE TABLE IF NOT EXISTS public.stock_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
batch_id UUID REFERENCES public.inventory_batches(id) ON DELETE SET NULL,
quantity_before NUMERIC(10, 3) NOT NULL,
quantity_change NUMERIC(10, 3) NOT NULL,
quantity_after NUMERIC(10, 3) NOT NULL CHECK (quantity_after >= 0),
reason stock_movement_reason NOT NULL,
note TEXT,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 10. Shopping Lists
CREATE TABLE IF NOT EXISTS public.shopping_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'Einkaufsliste',
status shopping_list_status NOT NULL DEFAULT 'ACTIVE',
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 11. Shopping List Items
CREATE TABLE IF NOT EXISTS public.shopping_list_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shopping_list_id UUID NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
custom_name TEXT,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0),
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
estimated_unit_price_cents INT CHECK (estimated_unit_price_cents >= 0),
selected_store TEXT,
checked BOOLEAN NOT NULL DEFAULT false,
automatically_added BOOLEAN NOT NULL DEFAULT false,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 12. Purchases
CREATE TABLE IF NOT EXISTS public.purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
store TEXT NOT NULL,
purchase_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
total_price_cents INT NOT NULL DEFAULT 0 CHECK (total_price_cents >= 0),
receipt_image_path TEXT,
notes TEXT,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 13. Purchase Items
CREATE TABLE IF NOT EXISTS public.purchase_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
purchase_id UUID NOT NULL REFERENCES public.purchases(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0),
unit_price_cents INT NOT NULL CHECK (unit_price_cents >= 0),
total_price_cents INT NOT NULL CHECK (total_price_cents >= 0),
package_size NUMERIC(10, 3),
package_unit TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 14. Price Observations
CREATE TABLE IF NOT EXISTS public.price_observations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
product_name TEXT NOT NULL,
brand TEXT,
package_size NUMERIC(10, 3),
package_unit TEXT,
price_cents INT NOT NULL CHECK (price_cents >= 0),
base_price_cents INT CHECK (base_price_cents >= 0),
base_price_unit TEXT,
retailer TEXT NOT NULL,
price_type price_type NOT NULL DEFAULT 'REGULAR',
postal_code TEXT,
region TEXT,
source_url TEXT,
source_title TEXT,
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_from DATE,
valid_until DATE,
confidence confidence_level NOT NULL DEFAULT 'MEDIUM',
exact_match BOOLEAN NOT NULL DEFAULT true,
verified BOOLEAN NOT NULL DEFAULT false,
source_kind source_kind NOT NULL DEFAULT 'USER_ESTIMATE',
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 15. Budgets
CREATE TABLE IF NOT EXISTS public.budgets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
period budget_period NOT NULL DEFAULT 'MONTHLY',
amount_cents INT NOT NULL CHECK (amount_cents >= 0),
warning_threshold_percent INT NOT NULL DEFAULT 80 CHECK (warning_threshold_percent BETWEEN 1 AND 100),
start_date DATE NOT NULL,
end_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 16. Inventory Sessions
CREATE TABLE IF NOT EXISTS public.inventory_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
status inventory_session_status NOT NULL DEFAULT 'OPEN',
started_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 17. Inventory Session Items
CREATE TABLE IF NOT EXISTS public.inventory_session_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES public.inventory_sessions(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
expected_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
counted_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
difference NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
confirmed BOOLEAN NOT NULL DEFAULT false,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 18. Dietary Preferences
CREATE TABLE IF NOT EXISTS public.dietary_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE,
diet_type diet_type NOT NULL DEFAULT 'OMNIVORE',
excluded_ingredients TEXT[] NOT NULL DEFAULT '{}',
allergens TEXT[] NOT NULL DEFAULT '{}',
preferred_cuisines TEXT[] NOT NULL DEFAULT '{}',
maximum_preparation_minutes INT,
maximum_meal_budget_cents INT,
default_servings INT NOT NULL DEFAULT 2,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 19. Recipes
CREATE TABLE IF NOT EXISTS public.recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
servings INT NOT NULL DEFAULT 2,
preparation_minutes INT,
difficulty TEXT,
estimated_cost_cents INT,
inventory_coverage_percent INT NOT NULL DEFAULT 0,
ingredients_json JSONB NOT NULL DEFAULT '[]'::jsonb,
steps_json JSONB NOT NULL DEFAULT '[]'::jsonb,
dietary_tags TEXT[] NOT NULL DEFAULT '{}',
allergen_warnings TEXT[] NOT NULL DEFAULT '{}',
source_information_json JSONB,
created_by_ai BOOLEAN NOT NULL DEFAULT false,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 20. Meal Plans
CREATE TABLE IF NOT EXISTS public.meal_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
estimated_total_cost_cents INT,
plan_json JSONB NOT NULL DEFAULT '[]'::jsonb,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 21. AI Settings
CREATE TABLE IF NOT EXISTS public.ai_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT true,
preferred_retailers TEXT[] NOT NULL DEFAULT '{"KAUFLAND", "REWE", "ALDI_NORD", "ALDI_SUED", "LIDL"}',
allow_google_search BOOLEAN NOT NULL DEFAULT true,
daily_request_limit INT NOT NULL DEFAULT 30 CHECK (daily_request_limit > 0),
automatic_price_updates BOOLEAN NOT NULL DEFAULT false,
price_update_interval_days INT NOT NULL DEFAULT 14 CHECK (price_update_interval_days > 0),
maximum_products_per_price_request INT NOT NULL DEFAULT 20 CHECK (maximum_products_per_price_request > 0),
save_chat_history BOOLEAN NOT NULL DEFAULT true,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 22. AI Request Logs
CREATE TABLE IF NOT EXISTS public.ai_request_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
feature TEXT NOT NULL,
model TEXT NOT NULL,
grounding_used BOOLEAN NOT NULL DEFAULT false,
input_token_count INT,
output_token_count INT,
latency_ms INT,
successful BOOLEAN NOT NULL DEFAULT true,
error_code TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 23. AI Pending Actions
CREATE TABLE IF NOT EXISTS public.ai_pending_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
action_type TEXT NOT NULL,
payload_json JSONB NOT NULL,
status pending_action_status NOT NULL DEFAULT 'PENDING',
expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '24 hours'),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
confirmed_at TIMESTAMPTZ
);
-- 24. Rate Limit Events
CREATE TABLE IF NOT EXISTS public.rate_limit_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
action TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Triggers for updated_at
CREATE TRIGGER trg_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_households_updated_at BEFORE UPDATE ON public.households FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_categories_updated_at BEFORE UPDATE ON public.categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON public.locations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON public.products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_inventory_batches_updated_at BEFORE UPDATE ON public.inventory_batches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_shopping_lists_updated_at BEFORE UPDATE ON public.shopping_lists FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_shopping_list_items_updated_at BEFORE UPDATE ON public.shopping_list_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_purchases_updated_at BEFORE UPDATE ON public.purchases FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_budgets_updated_at BEFORE UPDATE ON public.budgets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_inventory_session_items_updated_at BEFORE UPDATE ON public.inventory_session_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_dietary_preferences_updated_at BEFORE UPDATE ON public.dietary_preferences FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_recipes_updated_at BEFORE UPDATE ON public.recipes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_meal_plans_updated_at BEFORE UPDATE ON public.meal_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_ai_settings_updated_at BEFORE UPDATE ON public.ai_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Indexes for optimal query performance
CREATE INDEX idx_household_members_user ON public.household_members(user_id);
CREATE INDEX idx_products_household ON public.products(household_id);
CREATE INDEX idx_products_category ON public.products(category_id);
CREATE INDEX idx_products_location ON public.products(default_location_id);
CREATE INDEX idx_products_barcode ON public.products(barcode);
CREATE INDEX idx_inventory_batches_product ON public.inventory_batches(product_id);
CREATE INDEX idx_inventory_batches_household ON public.inventory_batches(household_id);
CREATE INDEX idx_inventory_batches_exp ON public.inventory_batches(expiration_date);
CREATE INDEX idx_stock_movements_product ON public.stock_movements(product_id);
CREATE INDEX idx_stock_movements_household ON public.stock_movements(household_id);
CREATE INDEX idx_shopping_list_items_list ON public.shopping_list_items(shopping_list_id);
CREATE INDEX idx_purchases_household_date ON public.purchases(household_id, purchase_date);
CREATE INDEX idx_price_observations_product ON public.price_observations(product_id);
CREATE INDEX idx_price_observations_observed ON public.price_observations(observed_at);
CREATE INDEX idx_rate_limit_events_user_action ON public.rate_limit_events(user_id, action, created_at);