Compare commits
2 Commits
main
...
37dc26bb14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37dc26bb14 | ||
|
|
441266683c |
282
supabase/migrations/001_initial_schema.sql
Normal file
282
supabase/migrations/001_initial_schema.sql
Normal file
@@ -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';
|
||||||
Reference in New Issue
Block a user