From 441266683cce8271293a56041de84fe5c5538f59 Mon Sep 17 00:00:00 2001 From: Claw Date: Mon, 9 Feb 2026 02:34:29 +0000 Subject: [PATCH] feat: add initial database schema migration (#13) - Create all core tables: inventory_items, products, tags, item_tags, units, user_profiles - Add ENUM types: tag_category, unit_type - Implement indexes for performance optimization - Add helper functions: update_updated_at(), convert_unit(), search_products() - Add triggers for automatic timestamp updates - Full-text search support for products - Comprehensive table comments for documentation Closes #13 --- supabase/migrations/001_initial_schema.sql | 282 +++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 supabase/migrations/001_initial_schema.sql diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql new file mode 100644 index 0000000..8ad0d62 --- /dev/null +++ b/supabase/migrations/001_initial_schema.sql @@ -0,0 +1,282 @@ +-- Migration: Initial database schema +-- Created: 2026-02-09 +-- Issue: #13 +-- Description: Creates core tables for Pantry app (inventory, products, tags, units) + +-- ====================== +-- ENUM TYPES +-- ====================== + +CREATE TYPE tag_category AS ENUM ( + 'position', -- Location: fridge, freezer, pantry + 'type', -- Food type: dairy, meat, vegan + 'dietary', -- Diet: gluten-free, vegan, organic + 'custom' -- User-defined +); + +CREATE TYPE unit_type AS ENUM ( + 'weight', -- kg, g, lb, oz + 'volume', -- L, mL, cup, tbsp + 'count', -- pcs, items (no conversion) + 'custom' -- can, jar, bottle (user-defined) +); + +-- ====================== +-- BASE TABLES (no foreign keys) +-- ====================== + +-- Units: measurement units with conversion support +CREATE TABLE units ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Core data + name TEXT NOT NULL, -- "kilogram", "liter", "piece" + abbreviation TEXT NOT NULL, -- "kg", "L", "pcs" + unit_type unit_type NOT NULL, + + -- Conversion system + base_unit_id UUID REFERENCES units(id), -- NULL = this is a base unit + conversion_factor DECIMAL(20,10), -- Factor to convert to base unit + + -- Metadata + is_default BOOLEAN DEFAULT false, -- Shipped with app + created_by UUID REFERENCES auth.users(id), -- NULL = system unit + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT unique_unit_abbr UNIQUE (abbreviation, created_by), + CONSTRAINT check_conversion_logic CHECK ( + (base_unit_id IS NULL AND conversion_factor IS NULL) OR + (base_unit_id IS NOT NULL AND conversion_factor IS NOT NULL) + ) +); + +-- Tags: flexible labeling system +CREATE TABLE tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Core data + name TEXT NOT NULL, + category tag_category NOT NULL DEFAULT 'custom', + + -- Visual + icon TEXT, -- Emoji or icon name: "🧊", "cheese" + color TEXT, -- Hex color: "#3b82f6" + + -- Ownership + created_by UUID REFERENCES auth.users(id), -- NULL = system tag + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT unique_tag_name UNIQUE (name, created_by) +); + +-- ====================== +-- MAIN TABLES +-- ====================== + +-- Products: cached product data from Open Food Facts +CREATE TABLE products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Open Food Facts data + barcode TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + brand TEXT, + image_url TEXT, + image_small_url TEXT, -- Thumbnail + + -- Categories from Open Food Facts + categories TEXT[], -- Array: ['dairy', 'milk'] + + -- Nutrition (optional, for future features) + nutrition JSONB, -- Full nutrition data + + -- Defaults + default_unit_id UUID REFERENCES units(id), + default_quantity DECIMAL(10,2), -- E.g., 1L bottle + + -- Metadata + source TEXT DEFAULT 'openfoodfacts', + cached_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_fetched TIMESTAMPTZ, + + -- Quality score (from Open Food Facts) + completeness_score INTEGER CHECK (completeness_score BETWEEN 0 AND 100) +); + +-- Inventory Items: actual items in your kitchen right now +CREATE TABLE inventory_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Product reference (nullable for custom items) + product_id UUID REFERENCES products(id) ON DELETE SET NULL, + + -- Core data + name TEXT NOT NULL, -- Product name or custom name + quantity DECIMAL(10,2) NOT NULL CHECK (quantity >= 0), + unit_id UUID NOT NULL REFERENCES units(id), + + -- Optional metadata + expiry_date DATE, + location TEXT, -- Free text: "top shelf", "door", etc. + notes TEXT, + + -- Audit trail + added_by UUID NOT NULL REFERENCES auth.users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Item Tags: many-to-many relationship between items and tags +CREATE TABLE item_tags ( + item_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE, + tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (item_id, tag_id) +); + +-- User Profiles: additional user metadata +CREATE TABLE user_profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + + display_name TEXT, + avatar_url TEXT, + + -- Preferences + default_unit_system TEXT DEFAULT 'metric', -- 'metric' or 'imperial' + theme TEXT DEFAULT 'auto', -- 'light', 'dark', 'auto' + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ====================== +-- INDEXES +-- ====================== + +-- Units +CREATE INDEX idx_units_type ON units(unit_type); +CREATE INDEX idx_units_base ON units(base_unit_id); + +-- Tags +CREATE INDEX idx_tags_category ON tags(category); +CREATE INDEX idx_tags_created_by ON tags(created_by); + +-- Products +CREATE UNIQUE INDEX idx_products_barcode ON products(barcode); +CREATE INDEX idx_products_name ON products USING GIN (to_tsvector('english', name)); +CREATE INDEX idx_products_search ON products + USING GIN (to_tsvector('english', name || ' ' || COALESCE(brand, ''))); + +-- Inventory Items +CREATE INDEX idx_items_product ON inventory_items(product_id); +CREATE INDEX idx_items_added_by ON inventory_items(added_by); +CREATE INDEX idx_items_expiry ON inventory_items(expiry_date) WHERE expiry_date IS NOT NULL; + +-- Item Tags +CREATE INDEX idx_item_tags_tag ON item_tags(tag_id); +CREATE INDEX idx_item_tags_item ON item_tags(item_id); + +-- ====================== +-- FUNCTIONS +-- ====================== + +-- Auto-update timestamp trigger function +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Unit conversion function +CREATE OR REPLACE FUNCTION convert_unit( + quantity DECIMAL, + from_unit_id UUID, + to_unit_id UUID +) +RETURNS DECIMAL AS $$ +DECLARE + from_factor DECIMAL; + to_factor DECIMAL; + from_type unit_type; + to_type unit_type; + base_quantity DECIMAL; +BEGIN + -- Get unit types and conversion factors + SELECT unit_type, + COALESCE(conversion_factor, 1.0) INTO from_type, from_factor + FROM units WHERE id = from_unit_id; + + SELECT unit_type, + COALESCE(conversion_factor, 1.0) INTO to_type, to_factor + FROM units WHERE id = to_unit_id; + + -- Check if units are compatible + IF from_type != to_type THEN + RAISE EXCEPTION 'Cannot convert between % and %', from_type, to_type; + END IF; + + -- Convert to base unit, then to target unit + base_quantity := quantity * from_factor; + RETURN base_quantity / to_factor; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Full-text search function for products +CREATE OR REPLACE FUNCTION search_products(search_query TEXT) +RETURNS TABLE ( + id UUID, + barcode TEXT, + name TEXT, + brand TEXT, + rank REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + p.id, + p.barcode, + p.name, + p.brand, + ts_rank(to_tsvector('english', p.name || ' ' || COALESCE(p.brand, '')), + plainto_tsquery('english', search_query)) AS rank + FROM products p + WHERE to_tsvector('english', p.name || ' ' || COALESCE(p.brand, '')) + @@ plainto_tsquery('english', search_query) + ORDER BY rank DESC + LIMIT 20; +END; +$$ LANGUAGE plpgsql; + +-- ====================== +-- TRIGGERS +-- ====================== + +-- Auto-update timestamp triggers +CREATE TRIGGER update_items_updated_at + BEFORE UPDATE ON inventory_items + FOR EACH ROW + EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_profiles_updated_at + BEFORE UPDATE ON user_profiles + FOR EACH ROW + EXECUTE FUNCTION update_updated_at(); + +-- ====================== +-- COMMENTS +-- ====================== + +COMMENT ON TABLE inventory_items IS 'Current inventory items in the kitchen'; +COMMENT ON TABLE products IS 'Cached product data from Open Food Facts'; +COMMENT ON TABLE tags IS 'Flexible labeling system for organization'; +COMMENT ON TABLE item_tags IS 'Many-to-many relationship between items and tags'; +COMMENT ON TABLE units IS 'Measurement units with conversion support'; +COMMENT ON TABLE user_profiles IS 'Additional user metadata and preferences'; + +COMMENT ON FUNCTION update_updated_at() IS 'Automatically updates the updated_at timestamp'; +COMMENT ON FUNCTION convert_unit(DECIMAL, UUID, UUID) IS 'Converts quantity between compatible units'; +COMMENT ON FUNCTION search_products(TEXT) IS 'Full-text search for products by name and brand';