-- ============================================ -- Storage Usage Table -- Tracks storage usage per tenant -- ============================================ CREATE TABLE storage.usage ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, -- Usage metrics total_files INTEGER NOT NULL DEFAULT 0 CHECK (total_files >= 0), total_bytes BIGINT NOT NULL DEFAULT 0 CHECK (total_bytes >= 0), -- By category files_by_type JSONB DEFAULT '{}', bytes_by_type JSONB DEFAULT '{}', -- Limits from plan max_bytes BIGINT, max_file_size BIGINT, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- One record per tenant CONSTRAINT uq_storage_usage_tenant UNIQUE (tenant_id) ); -- Indexes CREATE INDEX idx_storage_usage_tenant ON storage.usage(tenant_id); -- RLS ALTER TABLE storage.usage ENABLE ROW LEVEL SECURITY; CREATE POLICY storage_usage_tenant_isolation ON storage.usage FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Comments COMMENT ON TABLE storage.usage IS 'Aggregated storage usage per tenant'; COMMENT ON COLUMN storage.usage.files_by_type IS 'File count by mime type category: {images: 10, documents: 5}'; COMMENT ON COLUMN storage.usage.bytes_by_type IS 'Bytes by mime type category: {images: 1024000, documents: 512000}'; -- ============================================ -- Function: Update storage usage on file changes -- ============================================ CREATE OR REPLACE FUNCTION storage.update_usage_on_file_change() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' AND NEW.status = 'ready' THEN -- New file added INSERT INTO storage.usage (tenant_id, total_files, total_bytes) VALUES (NEW.tenant_id, 1, NEW.size_bytes) ON CONFLICT (tenant_id) DO UPDATE SET total_files = storage.usage.total_files + 1, total_bytes = storage.usage.total_bytes + NEW.size_bytes, updated_at = NOW(); ELSIF TG_OP = 'UPDATE' AND OLD.status = 'ready' AND NEW.status != 'ready' THEN -- File deleted/moved UPDATE storage.usage SET total_files = GREATEST(0, total_files - 1), total_bytes = GREATEST(0, total_bytes - OLD.size_bytes), updated_at = NOW() WHERE tenant_id = OLD.tenant_id; ELSIF TG_OP = 'DELETE' AND OLD.status = 'ready' THEN -- Hard delete UPDATE storage.usage SET total_files = GREATEST(0, total_files - 1), total_bytes = GREATEST(0, total_bytes - OLD.size_bytes), updated_at = NOW() WHERE tenant_id = OLD.tenant_id; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_storage_usage AFTER INSERT OR UPDATE OR DELETE ON storage.files FOR EACH ROW EXECUTE FUNCTION storage.update_usage_on_file_change(); COMMENT ON FUNCTION storage.update_usage_on_file_change IS 'Automatically update tenant storage usage'; -- ============================================ -- Function: Check if tenant can upload -- ============================================ CREATE OR REPLACE FUNCTION storage.can_upload( p_tenant_id UUID, p_size_bytes BIGINT ) RETURNS BOOLEAN AS $$ DECLARE v_current_bytes BIGINT; v_max_bytes BIGINT; v_max_file_size BIGINT; BEGIN SELECT total_bytes, max_bytes, max_file_size INTO v_current_bytes, v_max_bytes, v_max_file_size FROM storage.usage WHERE tenant_id = p_tenant_id; -- No usage record means no limit check needed yet IF NOT FOUND THEN RETURN TRUE; END IF; -- Check max file size IF v_max_file_size IS NOT NULL AND p_size_bytes > v_max_file_size THEN RETURN FALSE; END IF; -- Check total storage limit IF v_max_bytes IS NOT NULL AND (v_current_bytes + p_size_bytes) > v_max_bytes THEN RETURN FALSE; END IF; RETURN TRUE; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION storage.can_upload IS 'Check if tenant can upload file of given size'; -- ============================================ -- Function: Get tenant storage stats -- ============================================ CREATE OR REPLACE FUNCTION storage.get_tenant_stats(p_tenant_id UUID) RETURNS TABLE ( total_files INTEGER, total_bytes BIGINT, max_bytes BIGINT, max_file_size BIGINT, usage_percent NUMERIC, files_by_folder JSONB ) AS $$ BEGIN RETURN QUERY SELECT COALESCE(u.total_files, 0)::INTEGER, COALESCE(u.total_bytes, 0)::BIGINT, u.max_bytes, u.max_file_size, CASE WHEN u.max_bytes IS NULL OR u.max_bytes = 0 THEN 0 ELSE ROUND((u.total_bytes::NUMERIC / u.max_bytes * 100), 2) END, COALESCE( (SELECT jsonb_object_agg(folder, count) FROM ( SELECT folder, COUNT(*) as count FROM storage.files WHERE tenant_id = p_tenant_id AND deleted_at IS NULL GROUP BY folder ) t), '{}'::jsonb ) FROM storage.usage u WHERE u.tenant_id = p_tenant_id; -- If no record, return defaults IF NOT FOUND THEN RETURN QUERY SELECT 0, 0::BIGINT, NULL::BIGINT, NULL::BIGINT, 0::NUMERIC, '{}'::JSONB; END IF; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION storage.get_tenant_stats IS 'Get storage statistics for tenant';