diff --git a/.env.example b/.env.example index 2f4d499..bf83588 100644 --- a/.env.example +++ b/.env.example @@ -136,6 +136,15 @@ EMBEDDING_FAIL_OPEN=false #DEEPSQL_SKIP_AGENT_SETUP=0 #DEEPSQL_SMOKE_AGENT=1 +# ── Demo Data Seeding ─────────────────────────────────────────────────────── +# Seed a ready-to-explore demo database with sample e-commerce data, users, +# saved queries, slow query history, and index recommendations. +# Set to 1 to automatically run seed-demo-data.sh during install. +# Can also be run manually: ./scripts/self-host/seed-demo-data.sh +#DEEPSQL_SEED_DEMO_DATA=0 +#DEEPSQL_SEED_CONNECTION_NAME=Demo Shop (E-commerce) +#DEEPSQL_SEED_SKIP_DEMO_DB=0 + # ── The /api/llm/v1 agent gateway ─────────────────────────────────────────── # The DeepSQL CLI agent (`@deepsql/mcp`) points its model at /api/llm/v1 # and authenticates with a DeepSQL token; the backend forwards those calls diff --git a/README.md b/README.md index 374c642..c98dfc4 100644 --- a/README.md +++ b/README.md @@ -249,10 +249,28 @@ your schema — all from one shared brain. ```bash ./scripts/self-host/status.sh # compose ps + health probes ./scripts/self-host/smoke-test.sh # end-to-end check against the vault DB +./scripts/self-host/seed-demo-data.sh # seed demo e-commerce database for exploration ./scripts/self-host/uninstall.sh # stop and remove containers, keep data ./scripts/self-host/uninstall.sh --purge-data # also drop the volumes ``` +### Demo Database + +To explore DeepSQL features without connecting your own database, run: + +```bash +./scripts/self-host/seed-demo-data.sh +``` + +This creates a **demo_shop** e-commerce database with: +- 100 products, 500 customers, 5,000+ orders +- Intentionally suboptimal query patterns (to trigger recommendations) +- Pre-configured slow query analysis and index recommendations +- Sample saved queries in the SQL Editor +- Sample agent conversation history + +Alternatively, set `DEEPSQL_SEED_DEMO_DATA=1` in `.env` before running `install.sh` to seed automatically. + Upgrading: ```bash diff --git a/docker/postgres/init/10_create_demo_shop.sql b/docker/postgres/init/10_create_demo_shop.sql new file mode 100644 index 0000000..19d974e --- /dev/null +++ b/docker/postgres/init/10_create_demo_shop.sql @@ -0,0 +1,650 @@ +-- Demo Shop Database +-- A realistic e-commerce database for demonstrating DeepSQL features +-- Includes intentionally suboptimal patterns that will trigger recommendations + +-- Create the demo_shop database if it doesn't exist +SELECT 'Creating demo_shop database' AS status; + +-- Drop and recreate for idempotency +DROP DATABASE IF EXISTS demo_shop; +CREATE DATABASE demo_shop; + +\connect demo_shop + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + +-- ============================================================================ +-- SCHEMA: Core E-commerce Tables +-- ============================================================================ + +-- Categories table +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + parent_id INTEGER REFERENCES categories(id), + slug VARCHAR(100) UNIQUE NOT NULL, + is_active BOOLEAN DEFAULT true, + display_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Products table (intentionally missing composite index for common query patterns) +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + sku VARCHAR(50) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(10, 2) NOT NULL, + cost_price DECIMAL(10, 2), + category_id INTEGER REFERENCES categories(id), + brand VARCHAR(100), + weight_kg DECIMAL(8, 3), + is_active BOOLEAN DEFAULT true, + stock_quantity INTEGER DEFAULT 0, + low_stock_threshold INTEGER DEFAULT 10, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Single-column index (will be flagged as needing composite index) +CREATE INDEX idx_products_category ON products(category_id); +CREATE INDEX idx_products_is_active ON products(is_active); + +-- Customers table +CREATE TABLE customers ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + phone VARCHAR(20), + date_of_birth DATE, + gender VARCHAR(10), + loyalty_points INTEGER DEFAULT 0, + tier VARCHAR(20) DEFAULT 'bronze', -- bronze, silver, gold, platinum + is_verified BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + last_login_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_customers_email ON customers(email); +CREATE INDEX idx_customers_tier ON customers(tier); + +-- Addresses table +CREATE TABLE addresses ( + id SERIAL PRIMARY KEY, + customer_id INTEGER NOT NULL REFERENCES customers(id), + address_type VARCHAR(20) DEFAULT 'shipping', -- shipping, billing + street_address VARCHAR(255) NOT NULL, + city VARCHAR(100) NOT NULL, + state VARCHAR(100), + postal_code VARCHAR(20) NOT NULL, + country VARCHAR(100) NOT NULL DEFAULT 'United States', + is_default BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_addresses_customer ON addresses(customer_id); + +-- Orders table (will show as high-volume table needing optimization) +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + order_number VARCHAR(50) UNIQUE NOT NULL, + customer_id INTEGER REFERENCES customers(id), + status VARCHAR(30) NOT NULL DEFAULT 'pending', -- pending, confirmed, processing, shipped, delivered, cancelled, refunded + shipping_address_id INTEGER REFERENCES addresses(id), + billing_address_id INTEGER REFERENCES addresses(id), + subtotal DECIMAL(12, 2) NOT NULL, + tax_amount DECIMAL(12, 2) DEFAULT 0, + shipping_amount DECIMAL(12, 2) DEFAULT 0, + discount_amount DECIMAL(12, 2) DEFAULT 0, + total_amount DECIMAL(12, 2) NOT NULL, + currency VARCHAR(3) DEFAULT 'USD', + payment_method VARCHAR(50), + payment_status VARCHAR(30) DEFAULT 'pending', -- pending, paid, failed, refunded + notes TEXT, + shipped_at TIMESTAMP, + delivered_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Intentionally separate indexes (will be recommended to create composite) +CREATE INDEX idx_orders_customer ON orders(customer_id); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_orders_created ON orders(created_at); +CREATE INDEX idx_orders_payment_status ON orders(payment_status); + +-- Order items table +CREATE TABLE order_items ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + product_id INTEGER NOT NULL REFERENCES products(id), + quantity INTEGER NOT NULL, + unit_price DECIMAL(10, 2) NOT NULL, + subtotal DECIMAL(12, 2) NOT NULL, + discount_amount DECIMAL(10, 2) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_order_items_order ON order_items(order_id); +CREATE INDEX idx_order_items_product ON order_items(product_id); + +-- Product reviews table +CREATE TABLE product_reviews ( + id SERIAL PRIMARY KEY, + product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE, + customer_id INTEGER REFERENCES customers(id), + rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), + title VARCHAR(255), + review_text TEXT, + is_verified_purchase BOOLEAN DEFAULT false, + is_approved BOOLEAN DEFAULT false, + helpful_votes INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_reviews_product ON product_reviews(product_id); +CREATE INDEX idx_reviews_rating ON product_reviews(rating); + +-- Inventory movements table (for tracking stock changes) +CREATE TABLE inventory_movements ( + id SERIAL PRIMARY KEY, + product_id INTEGER NOT NULL REFERENCES products(id), + movement_type VARCHAR(30) NOT NULL, -- purchase, sale, adjustment, return, transfer + quantity INTEGER NOT NULL, + reference_id VARCHAR(100), -- order_id, purchase_order_id, etc. + notes TEXT, + created_by VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_inventory_product ON inventory_movements(product_id); +CREATE INDEX idx_inventory_type ON inventory_movements(movement_type); +CREATE INDEX idx_inventory_created ON inventory_movements(created_at); + +-- Coupons table +CREATE TABLE coupons ( + id SERIAL PRIMARY KEY, + code VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + discount_type VARCHAR(20) NOT NULL, -- percentage, fixed_amount + discount_value DECIMAL(10, 2) NOT NULL, + minimum_order_amount DECIMAL(10, 2), + max_uses INTEGER, + used_count INTEGER DEFAULT 0, + starts_at TIMESTAMP, + expires_at TIMESTAMP, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Order coupons junction table +CREATE TABLE order_coupons ( + order_id INTEGER NOT NULL REFERENCES orders(id), + coupon_id INTEGER NOT NULL REFERENCES coupons(id), + discount_applied DECIMAL(10, 2) NOT NULL, + PRIMARY KEY (order_id, coupon_id) +); + +-- Shopping cart table (for abandoned cart analysis) +CREATE TABLE shopping_carts ( + id SERIAL PRIMARY KEY, + customer_id INTEGER REFERENCES customers(id), + session_id VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP +); + +CREATE TABLE cart_items ( + id SERIAL PRIMARY KEY, + cart_id INTEGER NOT NULL REFERENCES shopping_carts(id) ON DELETE CASCADE, + product_id INTEGER NOT NULL REFERENCES products(id), + quantity INTEGER NOT NULL DEFAULT 1, + added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Audit log table (high-volume, will show growth concerns) +CREATE TABLE audit_log ( + id BIGSERIAL PRIMARY KEY, + table_name VARCHAR(100) NOT NULL, + record_id INTEGER NOT NULL, + action VARCHAR(20) NOT NULL, -- INSERT, UPDATE, DELETE + old_values JSONB, + new_values JSONB, + changed_by VARCHAR(100), + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Missing index on frequently queried columns (intentional for recommendations) +-- No index on table_name + changed_at composite + +-- ============================================================================ +-- SEED DATA: Categories +-- ============================================================================ + +INSERT INTO categories (name, description, slug, display_order) VALUES +('Electronics', 'Electronic devices and accessories', 'electronics', 1), +('Clothing', 'Apparel and fashion items', 'clothing', 2), +('Home & Garden', 'Home improvement and garden supplies', 'home-garden', 3), +('Sports & Outdoors', 'Sports equipment and outdoor gear', 'sports-outdoors', 4), +('Books', 'Books, e-books, and audiobooks', 'books', 5), +('Health & Beauty', 'Health, beauty, and personal care', 'health-beauty', 6); + +-- Subcategories +INSERT INTO categories (name, description, slug, parent_id, display_order) VALUES +('Smartphones', 'Mobile phones and accessories', 'smartphones', 1, 1), +('Laptops', 'Portable computers', 'laptops', 1, 2), +('Audio', 'Headphones, speakers, and audio equipment', 'audio', 1, 3), +('Men''s Clothing', 'Men''s apparel', 'mens-clothing', 2, 1), +('Women''s Clothing', 'Women''s apparel', 'womens-clothing', 2, 2), +('Furniture', 'Home furniture', 'furniture', 3, 1), +('Kitchen', 'Kitchen appliances and tools', 'kitchen', 3, 2); + +-- ============================================================================ +-- SEED DATA: Products (100 products across categories) +-- ============================================================================ + +INSERT INTO products (sku, name, description, price, cost_price, category_id, brand, stock_quantity, is_active) VALUES +-- Electronics - Smartphones +('ELEC-SP-001', 'ProPhone X15 Pro', '6.7" OLED display, 256GB storage, 5G capable', 1199.99, 750.00, 7, 'ProPhone', 150, true), +('ELEC-SP-002', 'ProPhone X15', '6.1" OLED display, 128GB storage, 5G capable', 999.99, 600.00, 7, 'ProPhone', 200, true), +('ELEC-SP-003', 'Galaxy Ultra S25', '6.8" Dynamic AMOLED, 512GB, S Pen included', 1299.99, 820.00, 7, 'Samsung', 100, true), +('ELEC-SP-004', 'Galaxy S25+', '6.6" Dynamic AMOLED, 256GB storage', 999.99, 620.00, 7, 'Samsung', 180, true), +('ELEC-SP-005', 'Pixel 9 Pro', '6.7" LTPO OLED, 256GB, Google AI features', 1099.99, 680.00, 7, 'Google', 120, true), +('ELEC-SP-006', 'OnePlus 13', '6.82" AMOLED, 256GB, Hasselblad camera', 899.99, 550.00, 7, 'OnePlus', 90, true), +('ELEC-SP-007', 'Budget Phone A5', '6.5" LCD, 64GB, Dual SIM', 199.99, 110.00, 7, 'BudgetTech', 500, true), +('ELEC-SP-008', 'ProPhone SE 4', '4.7" Retina, 128GB, compact design', 499.99, 300.00, 7, 'ProPhone', 250, true), + +-- Electronics - Laptops +('ELEC-LP-001', 'MacBook Pro 16" M4', '16" Liquid Retina XDR, M4 Pro chip, 32GB RAM', 2999.99, 2100.00, 8, 'Apple', 50, true), +('ELEC-LP-002', 'MacBook Air 15" M4', '15.3" Liquid Retina, M4 chip, 16GB RAM', 1499.99, 950.00, 8, 'Apple', 80, true), +('ELEC-LP-003', 'ThinkPad X1 Carbon Gen 12', '14" 2.8K OLED, Intel Core Ultra 9', 2199.99, 1500.00, 8, 'Lenovo', 60, true), +('ELEC-LP-004', 'Dell XPS 15', '15.6" OLED 3.5K, Intel Core i9, 32GB RAM', 2499.99, 1700.00, 8, 'Dell', 45, true), +('ELEC-LP-005', 'Surface Laptop 6', '13.5" PixelSense, Snapdragon X Elite', 1599.99, 1000.00, 8, 'Microsoft', 70, true), +('ELEC-LP-006', 'HP Spectre x360', '14" 2.8K OLED, Intel Core Ultra 7', 1799.99, 1150.00, 8, 'HP', 55, true), +('ELEC-LP-007', 'ASUS ROG Zephyrus G16', '16" QHD+ 240Hz, RTX 4090', 3299.99, 2400.00, 8, 'ASUS', 30, true), +('ELEC-LP-008', 'Chromebook Plus', '14" FHD, MediaTek, 8GB RAM', 449.99, 280.00, 8, 'Acer', 200, true), + +-- Electronics - Audio +('ELEC-AU-001', 'AirPods Pro 3', 'Active noise cancellation, spatial audio', 279.99, 150.00, 9, 'Apple', 300, true), +('ELEC-AU-002', 'Sony WH-1000XM6', 'Industry-leading ANC, 40hr battery', 399.99, 220.00, 9, 'Sony', 150, true), +('ELEC-AU-003', 'Bose QuietComfort Ultra', 'Premium ANC headphones', 449.99, 260.00, 9, 'Bose', 100, true), +('ELEC-AU-004', 'Galaxy Buds3 Pro', 'ANC earbuds with AI features', 249.99, 130.00, 9, 'Samsung', 200, true), +('ELEC-AU-005', 'JBL Charge 6', 'Portable Bluetooth speaker, waterproof', 179.99, 95.00, 9, 'JBL', 250, true), +('ELEC-AU-006', 'Sonos Era 300', 'Spatial audio smart speaker', 449.99, 280.00, 9, 'Sonos', 80, true), + +-- Clothing - Men's +('CLTH-MN-001', 'Classic Fit Oxford Shirt', '100% cotton, button-down collar', 59.99, 22.00, 10, 'Brooks Brothers', 400, true), +('CLTH-MN-002', 'Slim Fit Chinos', 'Stretch cotton, multiple colors', 79.99, 28.00, 10, 'Dockers', 350, true), +('CLTH-MN-003', 'Merino Wool Sweater', 'Fine gauge, crew neck', 129.99, 45.00, 10, 'J.Crew', 200, true), +('CLTH-MN-004', 'Performance Polo', 'Moisture-wicking, UPF 50+', 69.99, 25.00, 10, 'Nike', 500, true), +('CLTH-MN-005', 'Denim Jacket', 'Classic trucker style, medium wash', 98.99, 38.00, 10, 'Levi''s', 150, true), +('CLTH-MN-006', 'Quilted Vest', 'Lightweight insulation, water-resistant', 149.99, 55.00, 10, 'Patagonia', 120, true), + +-- Clothing - Women's +('CLTH-WN-001', 'Silk Blouse', '100% mulberry silk, relaxed fit', 189.99, 65.00, 11, 'Theory', 180, true), +('CLTH-WN-002', 'High-Rise Skinny Jeans', 'Premium stretch denim', 128.99, 42.00, 11, 'AG Jeans', 250, true), +('CLTH-WN-003', 'Cashmere Cardigan', 'Luxe knit, oversized fit', 298.99, 110.00, 11, 'Vince', 100, true), +('CLTH-WN-004', 'Midi Wrap Dress', 'Floral print, adjustable tie', 158.99, 52.00, 11, 'Diane von Furstenberg', 150, true), +('CLTH-WN-005', 'Leather Ankle Boots', 'Block heel, side zip', 249.99, 95.00, 11, 'Sam Edelman', 200, true), +('CLTH-WN-006', 'Puffer Jacket', 'Down-filled, packable', 199.99, 72.00, 11, 'The North Face', 180, true), + +-- Home & Garden - Furniture +('HOME-FR-001', 'Mid-Century Modern Sofa', '3-seater, linen upholstery', 1299.99, 580.00, 12, 'West Elm', 40, true), +('HOME-FR-002', 'Ergonomic Office Chair', 'Mesh back, lumbar support', 549.99, 220.00, 12, 'Herman Miller', 75, true), +('HOME-FR-003', 'Solid Oak Dining Table', '6-seater, expandable', 999.99, 420.00, 12, 'Crate & Barrel', 30, true), +('HOME-FR-004', 'Platform Bed Frame', 'King size, walnut finish', 799.99, 340.00, 12, 'Article', 50, true), +('HOME-FR-005', 'Modular Bookshelf', '5-tier, adjustable shelves', 349.99, 140.00, 12, 'IKEA', 100, true), +('HOME-FR-006', 'Velvet Accent Chair', 'Channel tufted, brass legs', 449.99, 180.00, 12, 'CB2', 60, true), + +-- Home & Garden - Kitchen +('HOME-KT-001', 'Stand Mixer Pro', '7-quart, 10 speeds, includes attachments', 449.99, 180.00, 13, 'KitchenAid', 120, true), +('HOME-KT-002', 'Dutch Oven 6Qt', 'Enameled cast iron, multiple colors', 379.99, 150.00, 13, 'Le Creuset', 90, true), +('HOME-KT-003', 'Chef''s Knife 8"', 'German steel, full tang', 149.99, 55.00, 13, 'Wüsthof', 200, true), +('HOME-KT-004', 'Espresso Machine', 'Semi-automatic, built-in grinder', 799.99, 350.00, 13, 'Breville', 60, true), +('HOME-KT-005', 'Air Fryer XL', '6-quart, digital controls', 129.99, 48.00, 13, 'Ninja', 300, true), +('HOME-KT-006', 'Non-Stick Pan Set', '10-piece, PFOA-free', 199.99, 75.00, 13, 'All-Clad', 150, true), + +-- Sports & Outdoors +('SPRT-001', 'Running Shoes Pro', 'Carbon fiber plate, responsive foam', 249.99, 95.00, 4, 'Nike', 400, true), +('SPRT-002', 'Yoga Mat Premium', '6mm thick, eco-friendly material', 89.99, 32.00, 4, 'Manduka', 250, true), +('SPRT-003', 'Fitness Tracker Ultra', 'GPS, heart rate, sleep tracking', 349.99, 140.00, 4, 'Garmin', 180, true), +('SPRT-004', 'Camping Tent 4-Person', 'Weatherproof, easy setup', 299.99, 120.00, 4, 'REI', 80, true), +('SPRT-005', 'Mountain Bike', '29" wheels, hydraulic brakes', 1499.99, 700.00, 4, 'Trek', 40, true), +('SPRT-006', 'Adjustable Dumbbells', '5-52.5 lbs, quick-change', 449.99, 200.00, 4, 'Bowflex', 100, true), +('SPRT-007', 'Golf Driver', 'Adjustable loft, carbon crown', 549.99, 250.00, 4, 'TaylorMade', 60, true), +('SPRT-008', 'Tennis Racket Pro', '100 sq in head, graphite frame', 229.99, 90.00, 4, 'Wilson', 90, true), + +-- Books +('BOOK-001', 'The Midnight Library', 'Bestselling fiction novel', 16.99, 7.00, 5, 'Penguin', 500, true), +('BOOK-002', 'Atomic Habits', 'Self-improvement bestseller', 18.99, 8.00, 5, 'Avery', 600, true), +('BOOK-003', 'Project Hail Mary', 'Science fiction adventure', 17.99, 7.50, 5, 'Ballantine', 400, true), +('BOOK-004', 'The Psychology of Money', 'Personal finance wisdom', 19.99, 8.50, 5, 'Harriman House', 450, true), +('BOOK-005', 'A Court of Thorns and Roses', 'Fantasy romance', 15.99, 6.50, 5, 'Bloomsbury', 550, true), +('BOOK-006', 'Dune', 'Classic science fiction', 18.99, 8.00, 5, 'Ace', 350, true), + +-- Health & Beauty +('HLTH-001', 'Retinol Serum', 'Anti-aging, 1% retinol', 68.00, 22.00, 6, 'The Ordinary', 300, true), +('HLTH-002', 'Vitamin C Moisturizer', 'Brightening, SPF 30', 54.00, 18.00, 6, 'CeraVe', 400, true), +('HLTH-003', 'Electric Toothbrush', 'Sonic, smart timer, travel case', 199.99, 80.00, 6, 'Philips', 200, true), +('HLTH-004', 'Hair Dryer Pro', 'Ionic technology, multiple speeds', 149.99, 55.00, 6, 'Dyson', 150, true), +('HLTH-005', 'Massage Gun', 'Percussion therapy, 6 attachments', 299.99, 120.00, 6, 'Theragun', 100, true), +('HLTH-006', 'Multivitamin Gummies', '90-day supply, mixed berry', 24.99, 9.00, 6, 'Olly', 600, true); + +-- Add more products to reach 100 +INSERT INTO products (sku, name, description, price, cost_price, category_id, brand, stock_quantity, is_active) +SELECT + 'PROD-' || LPAD(seq::text, 3, '0'), + 'Product ' || seq, + 'Description for product ' || seq, + ROUND((RANDOM() * 500 + 20)::numeric, 2), + ROUND((RANDOM() * 200 + 10)::numeric, 2), + (RANDOM() * 6 + 1)::integer, + CASE (RANDOM() * 5)::integer + WHEN 0 THEN 'BrandA' + WHEN 1 THEN 'BrandB' + WHEN 2 THEN 'BrandC' + WHEN 3 THEN 'BrandD' + ELSE 'BrandE' + END, + (RANDOM() * 500 + 10)::integer, + true +FROM generate_series(67, 100) AS seq; + +-- ============================================================================ +-- SEED DATA: Customers (500 customers) +-- ============================================================================ + +INSERT INTO customers (email, password_hash, first_name, last_name, phone, tier, loyalty_points, is_verified, is_active, created_at) +SELECT + 'customer' || seq || '@example.com', + '$2a$10$dummyhashforseeding' || seq, + CASE (seq % 20) + WHEN 0 THEN 'James' WHEN 1 THEN 'Mary' WHEN 2 THEN 'John' WHEN 3 THEN 'Patricia' + WHEN 4 THEN 'Robert' WHEN 5 THEN 'Jennifer' WHEN 6 THEN 'Michael' WHEN 7 THEN 'Linda' + WHEN 8 THEN 'William' WHEN 9 THEN 'Elizabeth' WHEN 10 THEN 'David' WHEN 11 THEN 'Barbara' + WHEN 12 THEN 'Richard' WHEN 13 THEN 'Susan' WHEN 14 THEN 'Joseph' WHEN 15 THEN 'Jessica' + WHEN 16 THEN 'Thomas' WHEN 17 THEN 'Sarah' WHEN 18 THEN 'Charles' ELSE 'Karen' + END, + CASE (seq % 15) + WHEN 0 THEN 'Smith' WHEN 1 THEN 'Johnson' WHEN 2 THEN 'Williams' WHEN 3 THEN 'Brown' + WHEN 4 THEN 'Jones' WHEN 5 THEN 'Garcia' WHEN 6 THEN 'Miller' WHEN 7 THEN 'Davis' + WHEN 8 THEN 'Rodriguez' WHEN 9 THEN 'Martinez' WHEN 10 THEN 'Hernandez' WHEN 11 THEN 'Lopez' + WHEN 12 THEN 'Gonzalez' WHEN 13 THEN 'Wilson' ELSE 'Anderson' + END, + '+1-555-' || LPAD((seq * 17 % 10000)::text, 4, '0'), + CASE + WHEN seq % 50 = 0 THEN 'platinum' + WHEN seq % 20 = 0 THEN 'gold' + WHEN seq % 5 = 0 THEN 'silver' + ELSE 'bronze' + END, + (RANDOM() * 10000)::integer, + true, + true, + CURRENT_TIMESTAMP - (RANDOM() * 730 || ' days')::interval +FROM generate_series(1, 500) AS seq; + +-- ============================================================================ +-- SEED DATA: Addresses +-- ============================================================================ + +INSERT INTO addresses (customer_id, address_type, street_address, city, state, postal_code, country, is_default) +SELECT + c.id, + 'shipping', + (100 + (c.id * 7) % 9900)::text || ' ' || + CASE (c.id % 10) WHEN 0 THEN 'Main' WHEN 1 THEN 'Oak' WHEN 2 THEN 'Maple' WHEN 3 THEN 'Cedar' + WHEN 4 THEN 'Pine' WHEN 5 THEN 'Elm' WHEN 6 THEN 'Park' WHEN 7 THEN 'Lake' + WHEN 8 THEN 'River' ELSE 'Hill' END || ' ' || + CASE (c.id % 5) WHEN 0 THEN 'Street' WHEN 1 THEN 'Avenue' WHEN 2 THEN 'Boulevard' WHEN 3 THEN 'Drive' ELSE 'Lane' END, + CASE (c.id % 12) + WHEN 0 THEN 'New York' WHEN 1 THEN 'Los Angeles' WHEN 2 THEN 'Chicago' WHEN 3 THEN 'Houston' + WHEN 4 THEN 'Phoenix' WHEN 5 THEN 'Philadelphia' WHEN 6 THEN 'San Antonio' WHEN 7 THEN 'San Diego' + WHEN 8 THEN 'Dallas' WHEN 9 THEN 'San Jose' WHEN 10 THEN 'Austin' ELSE 'Jacksonville' + END, + CASE (c.id % 12) + WHEN 0 THEN 'NY' WHEN 1 THEN 'CA' WHEN 2 THEN 'IL' WHEN 3 THEN 'TX' + WHEN 4 THEN 'AZ' WHEN 5 THEN 'PA' WHEN 6 THEN 'TX' WHEN 7 THEN 'CA' + WHEN 8 THEN 'TX' WHEN 9 THEN 'CA' WHEN 10 THEN 'TX' ELSE 'FL' + END, + LPAD((10000 + (c.id * 13) % 89999)::text, 5, '0'), + 'United States', + true +FROM customers c; + +-- ============================================================================ +-- SEED DATA: Orders (5000 orders over the last 2 years) +-- ============================================================================ + +INSERT INTO orders (order_number, customer_id, status, shipping_address_id, subtotal, tax_amount, shipping_amount, total_amount, payment_method, payment_status, created_at) +SELECT + 'ORD-' || TO_CHAR(CURRENT_DATE - (seq / 7 || ' days')::interval, 'YYYYMMDD') || '-' || LPAD(seq::text, 5, '0'), + (RANDOM() * 499 + 1)::integer, + CASE (seq % 10) + WHEN 0 THEN 'pending' + WHEN 1 THEN 'confirmed' + WHEN 2 THEN 'processing' + WHEN 3 THEN 'shipped' + WHEN 4 THEN 'shipped' + WHEN 5 THEN 'delivered' + WHEN 6 THEN 'delivered' + WHEN 7 THEN 'delivered' + WHEN 8 THEN 'delivered' + ELSE 'cancelled' + END, + (RANDOM() * 499 + 1)::integer, + ROUND((RANDOM() * 500 + 50)::numeric, 2), + ROUND((RANDOM() * 50 + 5)::numeric, 2), + CASE WHEN RANDOM() > 0.3 THEN 0 ELSE ROUND((RANDOM() * 20 + 5)::numeric, 2) END, + 0, -- Will be calculated + CASE (seq % 4) + WHEN 0 THEN 'credit_card' + WHEN 1 THEN 'paypal' + WHEN 2 THEN 'apple_pay' + ELSE 'credit_card' + END, + CASE + WHEN seq % 10 = 0 THEN 'pending' + WHEN seq % 10 = 9 THEN 'refunded' + ELSE 'paid' + END, + CURRENT_TIMESTAMP - (seq / 7 || ' days')::interval - (RANDOM() * 6 || ' hours')::interval +FROM generate_series(1, 5000) AS seq; + +-- Update total_amount +UPDATE orders SET total_amount = subtotal + tax_amount + shipping_amount - discount_amount; + +-- ============================================================================ +-- SEED DATA: Order Items +-- ============================================================================ + +INSERT INTO order_items (order_id, product_id, quantity, unit_price, subtotal) +SELECT + o.id, + p.id, + (RANDOM() * 3 + 1)::integer, + p.price, + p.price * (RANDOM() * 3 + 1)::integer +FROM orders o +CROSS JOIN LATERAL ( + SELECT id, price FROM products + WHERE is_active = true + ORDER BY RANDOM() + LIMIT (RANDOM() * 4 + 1)::integer +) p; + +-- ============================================================================ +-- SEED DATA: Product Reviews +-- ============================================================================ + +INSERT INTO product_reviews (product_id, customer_id, rating, title, review_text, is_verified_purchase, is_approved, helpful_votes, created_at) +SELECT + p.id, + (seq % 500) + 1, + (RANDOM() * 4 + 1)::integer, + CASE (seq % 10) + WHEN 0 THEN 'Great product!' + WHEN 1 THEN 'Exceeded expectations' + WHEN 2 THEN 'Good value for money' + WHEN 3 THEN 'Works as advertised' + WHEN 4 THEN 'Highly recommend' + WHEN 5 THEN 'Not what I expected' + WHEN 6 THEN 'Decent quality' + WHEN 7 THEN 'Fast shipping' + WHEN 8 THEN 'Perfect fit' + ELSE 'Would buy again' + END, + 'This is a review for product. ' || + CASE (seq % 5) + WHEN 0 THEN 'I love it and would definitely recommend to friends and family.' + WHEN 1 THEN 'It arrived quickly and was exactly as described.' + WHEN 2 THEN 'Good quality for the price point.' + WHEN 3 THEN 'Some minor issues but overall satisfied.' + ELSE 'Excellent customer service when I had questions.' + END, + RANDOM() > 0.3, + RANDOM() > 0.1, + (RANDOM() * 50)::integer, + CURRENT_TIMESTAMP - (RANDOM() * 365 || ' days')::interval +FROM generate_series(1, 1500) AS seq +CROSS JOIN LATERAL (SELECT id FROM products ORDER BY RANDOM() LIMIT 1) p; + +-- ============================================================================ +-- SEED DATA: Inventory Movements +-- ============================================================================ + +INSERT INTO inventory_movements (product_id, movement_type, quantity, reference_id, notes, created_by, created_at) +SELECT + p.id, + CASE (seq % 5) + WHEN 0 THEN 'purchase' + WHEN 1 THEN 'sale' + WHEN 2 THEN 'sale' + WHEN 3 THEN 'sale' + ELSE 'adjustment' + END, + CASE + WHEN seq % 5 = 0 THEN (RANDOM() * 100 + 10)::integer + ELSE -(RANDOM() * 5 + 1)::integer + END, + CASE WHEN seq % 5 IN (1,2,3) THEN 'ORD-' || (RANDOM() * 5000 + 1)::integer ELSE NULL END, + CASE WHEN seq % 5 = 4 THEN 'Inventory count adjustment' ELSE NULL END, + 'system', + CURRENT_TIMESTAMP - (RANDOM() * 180 || ' days')::interval +FROM generate_series(1, 10000) AS seq +CROSS JOIN LATERAL (SELECT id FROM products ORDER BY RANDOM() LIMIT 1) p; + +-- ============================================================================ +-- SEED DATA: Coupons +-- ============================================================================ + +INSERT INTO coupons (code, description, discount_type, discount_value, minimum_order_amount, max_uses, starts_at, expires_at, is_active) VALUES +('WELCOME10', 'New customer 10% off', 'percentage', 10, 50, 1000, CURRENT_TIMESTAMP - INTERVAL '30 days', CURRENT_TIMESTAMP + INTERVAL '365 days', true), +('SUMMER20', 'Summer sale 20% off', 'percentage', 20, 100, 500, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP + INTERVAL '45 days', true), +('FLAT25', '$25 off orders over $150', 'fixed_amount', 25, 150, 200, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '30 days', true), +('VIP15', 'VIP member discount', 'percentage', 15, NULL, NULL, CURRENT_TIMESTAMP - INTERVAL '90 days', CURRENT_TIMESTAMP + INTERVAL '275 days', true), +('FREESHIP', 'Free shipping on orders $75+', 'fixed_amount', 10, 75, 1000, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP + INTERVAL '23 days', true); + +-- ============================================================================ +-- SEED DATA: Audit Log (for growth pattern analysis) +-- ============================================================================ + +INSERT INTO audit_log (table_name, record_id, action, new_values, changed_by, changed_at) +SELECT + CASE (seq % 5) + WHEN 0 THEN 'orders' + WHEN 1 THEN 'customers' + WHEN 2 THEN 'products' + WHEN 3 THEN 'order_items' + ELSE 'inventory_movements' + END, + (RANDOM() * 5000 + 1)::integer, + CASE (seq % 3) WHEN 0 THEN 'INSERT' WHEN 1 THEN 'UPDATE' ELSE 'INSERT' END, + '{"status": "updated"}'::jsonb, + 'system', + CURRENT_TIMESTAMP - (seq / 100 || ' hours')::interval +FROM generate_series(1, 50000) AS seq; + +-- ============================================================================ +-- VIEWS: Useful aggregations (candidates for materialized views) +-- ============================================================================ + +-- Daily revenue view (good candidate for MV recommendation) +CREATE VIEW v_daily_revenue AS +SELECT + DATE(created_at) as order_date, + COUNT(*) as total_orders, + SUM(total_amount) as total_revenue, + AVG(total_amount) as avg_order_value, + COUNT(DISTINCT customer_id) as unique_customers +FROM orders +WHERE status NOT IN ('cancelled', 'refunded') +GROUP BY DATE(created_at); + +-- Product performance view +CREATE VIEW v_product_performance AS +SELECT + p.id, + p.name, + p.category_id, + COUNT(oi.id) as times_ordered, + SUM(oi.quantity) as units_sold, + SUM(oi.subtotal) as total_revenue, + AVG(r.rating) as avg_rating, + COUNT(r.id) as review_count +FROM products p +LEFT JOIN order_items oi ON p.id = oi.product_id +LEFT JOIN product_reviews r ON p.id = r.product_id +GROUP BY p.id, p.name, p.category_id; + +-- Customer lifetime value view +CREATE VIEW v_customer_ltv AS +SELECT + c.id, + c.email, + c.tier, + COUNT(o.id) as total_orders, + SUM(o.total_amount) as lifetime_value, + AVG(o.total_amount) as avg_order_value, + MAX(o.created_at) as last_order_date, + MIN(o.created_at) as first_order_date +FROM customers c +LEFT JOIN orders o ON c.id = o.customer_id AND o.status NOT IN ('cancelled', 'refunded') +GROUP BY c.id, c.email, c.tier; + +-- ============================================================================ +-- ANALYZE tables for statistics +-- ============================================================================ + +ANALYZE categories; +ANALYZE products; +ANALYZE customers; +ANALYZE addresses; +ANALYZE orders; +ANALYZE order_items; +ANALYZE product_reviews; +ANALYZE inventory_movements; +ANALYZE coupons; +ANALYZE audit_log; + +-- ============================================================================ +-- Grant permissions to postgres user +-- ============================================================================ + +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO postgres; + +SELECT 'Demo shop database created successfully!' AS status; +SELECT 'Tables: ' || COUNT(*)::text FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'; +SELECT 'Products: ' || COUNT(*)::text FROM products; +SELECT 'Customers: ' || COUNT(*)::text FROM customers; +SELECT 'Orders: ' || COUNT(*)::text FROM orders; diff --git a/scripts/self-host/install.sh b/scripts/self-host/install.sh index a1be91d..fbb452d 100755 --- a/scripts/self-host/install.sh +++ b/scripts/self-host/install.sh @@ -519,9 +519,32 @@ report_cli_status() { report_cli_status +# ── Demo Data Seeding ───────────────────────────────────────────────────────── +# Optional: seed a demo database with sample e-commerce data, users, saved queries, +# and performance recommendations. Gives new users an end-to-end view of all features. +# Enable with DEEPSQL_SEED_DEMO_DATA=1 in .env or environment. +if [[ "${DEEPSQL_SEED_DEMO_DATA:-0}" == "1" ]]; then + if [[ -x "$SCRIPT_DIR/seed-demo-data.sh" ]]; then + echo "Seeding demo data (DEEPSQL_SEED_DEMO_DATA=1)…" + if "$SCRIPT_DIR/seed-demo-data.sh"; then + echo "Demo data seeding complete." + else + echo "Warning: demo data seeding failed. The stack still works, but the demo" >&2 + echo " database and sample data were not created. Run manually:" >&2 + echo " ./scripts/self-host/seed-demo-data.sh" >&2 + fi + echo + fi +else + echo "Demo data seeding skipped (set DEEPSQL_SEED_DEMO_DATA=1 to enable)." + echo " Run ./scripts/self-host/seed-demo-data.sh for a ready-to-explore demo database." + echo +fi + echo "Useful commands:" echo " ./scripts/self-host/status.sh" echo " ./scripts/self-host/smoke-test.sh" echo " ./scripts/self-host/setup-agent.sh # Hermes webui + MCP profile" +echo " ./scripts/self-host/seed-demo-data.sh # Seed demo e-commerce database" echo " python3 scripts/self-host/e2e-agent-check.py # live Agent+dashboard turn" echo " ./scripts/self-host/uninstall.sh" diff --git a/scripts/self-host/seed-demo-data.sh b/scripts/self-host/seed-demo-data.sh new file mode 100755 index 0000000..d513561 --- /dev/null +++ b/scripts/self-host/seed-demo-data.sh @@ -0,0 +1,777 @@ +#!/usr/bin/env bash +# seed-demo-data.sh — Seeds the DeepSQL installation with a demo database and sample data +# +# This script creates: +# - demo_shop: A realistic e-commerce database with customers, orders, products +# - Demo users: analyst, developer, viewer with scoped access +# - Sample saved queries in the SQL editor +# - Sample slow query analysis and index recommendations +# - Sample agent conversation history +# +# Run after install.sh completes. Requires the stack to be running. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +ENV_FILE="${DEEPSQL_ENV_FILE:-$ROOT_DIR/.env}" +COMPOSE_FILE="${DEEPSQL_COMPOSE_FILE:-$ROOT_DIR/docker-compose.yml}" +PROJECT_NAME="${DEEPSQL_PROJECT_NAME:-deepsql-selfhost}" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Error: missing env file $ENV_FILE" >&2 + echo "Run install.sh first to set up the stack." >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +set -a +source "$ENV_FILE" +set +a + +: "${DEEPSQL_BACKEND_PORT:=8080}" +: "${DB_PASSWORD:=postgres}" +: "${DEEPSQL_INITIAL_ADMIN_EMAIL:=}" +: "${DEEPSQL_INITIAL_ADMIN_PASSWORD:=}" +: "${DEEPSQL_SEED_SKIP_DEMO_DB:=0}" +: "${DEEPSQL_SEED_CONNECTION_NAME:=Demo Shop (E-commerce)}" + +compose() { + DEEPSQL_RUNTIME_ENV_FILE="$ENV_FILE" docker compose \ + --project-name "$PROJECT_NAME" \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + "$@" +} + +echo "==========================================" +echo "DeepSQL Demo Data Seeding" +echo "==========================================" + +# ============================================================================ +# Step 1: Create demo_shop database +# ============================================================================ + +if [[ "$DEEPSQL_SEED_SKIP_DEMO_DB" != "1" ]]; then + echo "" + echo "Step 1: Creating demo_shop database..." + + # Check if demo_shop already exists + demo_exists="$(compose exec -T postgres psql -U postgres -At -c "SELECT 1 FROM pg_database WHERE datname = 'demo_shop'" 2>/dev/null || echo "")" + + if [[ "$demo_exists" == "1" ]]; then + echo " demo_shop database already exists. Skipping creation." + echo " (Set DEEPSQL_SEED_SKIP_DEMO_DB=1 to always skip, or drop the database to recreate)" + else + demo_sql="$ROOT_DIR/docker/postgres/init/10_create_demo_shop.sql" + if [[ -f "$demo_sql" ]]; then + echo " Running demo_shop creation script..." + compose exec -T postgres psql -U postgres -f /docker-entrypoint-initdb.d/10_create_demo_shop.sql >/dev/null 2>&1 || \ + compose exec -T postgres psql -U postgres < "$demo_sql" + echo " demo_shop database created successfully." + else + echo " Warning: demo_shop SQL script not found at $demo_sql" + echo " Skipping demo database creation." + fi + fi +else + echo "Step 1: Skipping demo_shop database creation (DEEPSQL_SEED_SKIP_DEMO_DB=1)" +fi + +# ============================================================================ +# Step 2: Authenticate as admin +# ============================================================================ + +echo "" +echo "Step 2: Authenticating as admin..." + +if [[ -z "$DEEPSQL_INITIAL_ADMIN_EMAIL" || -z "$DEEPSQL_INITIAL_ADMIN_PASSWORD" ]]; then + echo "Error: DEEPSQL_INITIAL_ADMIN_EMAIL and DEEPSQL_INITIAL_ADMIN_PASSWORD must be set." >&2 + exit 1 +fi + +base="http://localhost:${DEEPSQL_BACKEND_PORT}/api" +cookie_jar="$(mktemp)" +trap 'rm -f "$cookie_jar"' EXIT + +login_deadline=$((SECONDS + 60)) +while (( SECONDS < login_deadline )); do + if login_json="$(curl -fsS -c "$cookie_jar" -H 'Content-Type: application/json' \ + -X POST "$base/auth/login" \ + -d "{\"email\":\"${DEEPSQL_INITIAL_ADMIN_EMAIL}\",\"password\":\"${DEEPSQL_INITIAL_ADMIN_PASSWORD}\"}" 2>/dev/null)"; then + if [[ "$login_json" == *"\"email\""* ]]; then + echo " Logged in as admin." + break + fi + fi + sleep 2 +done + +if [[ "${login_json:-}" != *"\"email\""* ]]; then + echo "Error: Could not authenticate as admin." >&2 + exit 1 +fi + +# ============================================================================ +# Step 3: Create demo connection to demo_shop +# ============================================================================ + +echo "" +echo "Step 3: Creating demo connection..." + +# Check if connection already exists +existing_conn="$(curl -fsS -b "$cookie_jar" "$base/connections" 2>/dev/null || echo "[]")" +if [[ "$existing_conn" == *"$DEEPSQL_SEED_CONNECTION_NAME"* ]]; then + echo " Demo connection already exists. Extracting connection ID..." + connection_id="$(printf '%s' "$existing_conn" | python3 -c " +import sys, json +data = json.load(sys.stdin) +for conn in data: + if conn.get('connectionName') == '$DEEPSQL_SEED_CONNECTION_NAME': + print(conn.get('id', '')) + break +" 2>/dev/null || echo "")" + if [[ -n "$connection_id" ]]; then + echo " Using existing connection: $connection_id" + fi +else + payload=$(cat </dev/null || echo "{}")" + connection_id="$(printf '%s' "$save_json" | sed -n 's/.*"connectionId":"\([^"]*\)".*/\1/p')" + + if [[ -z "$connection_id" ]]; then + echo " Warning: Could not create demo connection." + echo " Response: $save_json" + echo " Continuing with other seed data..." + else + echo " Created demo connection: $connection_id" + fi +fi + +# ============================================================================ +# Step 4: Create demo users +# ============================================================================ + +echo "" +echo "Step 4: Creating demo users..." + +create_user() { + local username="$1" + local email="$2" + local password="$3" + local role="$4" + + # Check if user exists + users_json="$(curl -fsS -b "$cookie_jar" "$base/users" 2>/dev/null || echo "[]")" + if [[ "$users_json" == *"\"$email\""* ]]; then + echo " User $username already exists, skipping." + return 0 + fi + + # Create user via invite flow or direct insert + # Note: Direct user creation may require admin privileges + local user_payload + user_payload=$(cat </dev/null || echo "{}")" + + if [[ "$result" == *"error"* || "$result" == *"Error"* ]]; then + echo " Note: Could not create user $username via API (may need manual creation)" + else + echo " Created user: $username ($email) with role $role" + fi +} + +# Create demo users +create_user "analyst" "analyst@demo.local" "analyst123!" "DEVELOPER" +create_user "developer" "developer@demo.local" "developer123!" "DEVELOPER" +create_user "viewer" "viewer@demo.local" "viewer123!" "DEVELOPER" + +# ============================================================================ +# Step 5: Create saved queries +# ============================================================================ + +echo "" +echo "Step 5: Creating saved queries..." + +if [[ -n "${connection_id:-}" ]]; then + create_saved_query() { + local name="$1" + local query="$2" + local description="$3" + local folder="$4" + local tags="$5" + local is_favorite="$6" + + local payload + payload=$(cat </dev/null || echo "{}")" + + if [[ "$result" == *"\"id\""* ]]; then + echo " Created: $name" + else + echo " Note: Could not create query '$name'" + fi + } + + # Revenue & Sales Queries + create_saved_query \ + "Daily Revenue Report" \ + "SELECT DATE(created_at) as order_date, COUNT(*) as total_orders, SUM(total_amount) as revenue, AVG(total_amount) as avg_order_value FROM orders WHERE status NOT IN ('cancelled', 'refunded') AND created_at >= CURRENT_DATE - INTERVAL '30 days' GROUP BY DATE(created_at) ORDER BY order_date DESC;" \ + "Shows daily revenue for the last 30 days" \ + "Reports" \ + "revenue,daily,sales" \ + true + + create_saved_query \ + "Top Products by Revenue" \ + "SELECT p.name, p.sku, COUNT(oi.id) as times_ordered, SUM(oi.quantity) as units_sold, SUM(oi.subtotal) as total_revenue FROM products p JOIN order_items oi ON p.id = oi.product_id JOIN orders o ON oi.order_id = o.id WHERE o.status NOT IN ('cancelled', 'refunded') GROUP BY p.id, p.name, p.sku ORDER BY total_revenue DESC LIMIT 20;" \ + "Top 20 products by revenue" \ + "Reports" \ + "products,revenue,top" \ + true + + create_saved_query \ + "Customer Lifetime Value" \ + "SELECT c.email, c.tier, COUNT(o.id) as total_orders, SUM(o.total_amount) as lifetime_value, AVG(o.total_amount) as avg_order_value, MAX(o.created_at) as last_order FROM customers c LEFT JOIN orders o ON c.id = o.customer_id AND o.status NOT IN ('cancelled', 'refunded') GROUP BY c.id, c.email, c.tier HAVING COUNT(o.id) > 0 ORDER BY lifetime_value DESC LIMIT 50;" \ + "Top 50 customers by lifetime value" \ + "Reports" \ + "customers,ltv,analysis" \ + true + + # Operations Queries + create_saved_query \ + "Low Stock Products" \ + "SELECT p.sku, p.name, p.stock_quantity, p.low_stock_threshold, c.name as category FROM products p JOIN categories c ON p.category_id = c.id WHERE p.stock_quantity <= p.low_stock_threshold AND p.is_active = true ORDER BY p.stock_quantity ASC;" \ + "Products below their low stock threshold" \ + "Operations" \ + "inventory,stock,alerts" \ + false + + create_saved_query \ + "Orders Pending Shipment" \ + "SELECT o.order_number, o.created_at, o.total_amount, c.email, c.first_name, c.last_name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.status IN ('confirmed', 'processing') AND o.payment_status = 'paid' ORDER BY o.created_at ASC;" \ + "Paid orders waiting to be shipped" \ + "Operations" \ + "orders,shipping,pending" \ + false + + create_saved_query \ + "Recent Inventory Movements" \ + "SELECT im.created_at, p.sku, p.name, im.movement_type, im.quantity, im.reference_id, im.notes FROM inventory_movements im JOIN products p ON im.product_id = p.id WHERE im.created_at >= CURRENT_DATE - INTERVAL '7 days' ORDER BY im.created_at DESC LIMIT 100;" \ + "Inventory changes in the last 7 days" \ + "Operations" \ + "inventory,movements,audit" \ + false + + # Analytics Queries + create_saved_query \ + "Category Performance" \ + "SELECT c.name as category, COUNT(DISTINCT p.id) as products, COUNT(oi.id) as orders, SUM(oi.subtotal) as revenue FROM categories c LEFT JOIN products p ON c.id = p.category_id LEFT JOIN order_items oi ON p.id = oi.product_id LEFT JOIN orders o ON oi.order_id = o.id AND o.status NOT IN ('cancelled', 'refunded') WHERE c.parent_id IS NULL GROUP BY c.id, c.name ORDER BY revenue DESC NULLS LAST;" \ + "Revenue and order counts by top-level category" \ + "Analytics" \ + "categories,performance,analysis" \ + false + + create_saved_query \ + "Customer Tier Distribution" \ + "SELECT tier, COUNT(*) as customer_count, AVG(loyalty_points) as avg_points, SUM(loyalty_points) as total_points FROM customers WHERE is_active = true GROUP BY tier ORDER BY CASE tier WHEN 'platinum' THEN 1 WHEN 'gold' THEN 2 WHEN 'silver' THEN 3 ELSE 4 END;" \ + "Customer distribution across loyalty tiers" \ + "Analytics" \ + "customers,tiers,loyalty" \ + false + + create_saved_query \ + "Hourly Order Distribution" \ + "SELECT EXTRACT(HOUR FROM created_at) as hour_of_day, COUNT(*) as order_count, SUM(total_amount) as revenue FROM orders WHERE created_at >= CURRENT_DATE - INTERVAL '30 days' AND status NOT IN ('cancelled') GROUP BY EXTRACT(HOUR FROM created_at) ORDER BY hour_of_day;" \ + "Order volume by hour of day (last 30 days)" \ + "Analytics" \ + "orders,hourly,patterns" \ + false + + # Slow Query Examples (intentionally suboptimal for recommendations) + create_saved_query \ + "[Example] Unoptimized Full Table Scan" \ + "SELECT * FROM orders WHERE LOWER(status) = 'delivered' AND total_amount > 100 ORDER BY created_at DESC;" \ + "Example of a query that could benefit from index optimization (uses LOWER() preventing index use)" \ + "Examples" \ + "slow,example,optimization" \ + false + + create_saved_query \ + "[Example] Missing Index Pattern" \ + "SELECT o.*, c.email, c.first_name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.status = 'pending' AND o.payment_status = 'paid' AND o.created_at > CURRENT_DATE - INTERVAL '7 days';" \ + "Query that would benefit from a composite index on (status, payment_status, created_at)" \ + "Examples" \ + "slow,example,index" \ + false + + create_saved_query \ + "[Example] Aggregation Candidate for MV" \ + "SELECT DATE_TRUNC('month', o.created_at) as month, c.name as category, COUNT(DISTINCT o.id) as orders, SUM(oi.subtotal) as revenue, COUNT(DISTINCT o.customer_id) as customers FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id JOIN categories c ON p.category_id = c.id WHERE o.status NOT IN ('cancelled', 'refunded') GROUP BY DATE_TRUNC('month', o.created_at), c.id, c.name ORDER BY month DESC, revenue DESC;" \ + "Monthly category revenue - candidate for materialized view" \ + "Examples" \ + "slow,example,materialized-view" \ + false +else + echo " Skipping saved queries (no connection ID available)" +fi + +# ============================================================================ +# Step 6: Seed slow query history and recommendations via SQL +# ============================================================================ + +echo "" +echo "Step 6: Seeding performance data (slow queries, recommendations)..." + +if [[ -n "${connection_id:-}" ]]; then + # Generate sample slow query analysis data + slow_query_json=$(cat <<'SQJSON' +{ + "slowQueries": [ + { + "query": "SELECT * FROM orders WHERE LOWER(status) = 'delivered' AND total_amount > 100", + "executionTime": 2340.5, + "calls": 1250, + "meanTime": 1.87, + "maxTime": 45.2, + "rows": 15000, + "severity": "HIGH", + "pattern": "FULL_TABLE_SCAN", + "suggestion": "Add index on orders(status) and avoid LOWER() function on indexed column" + }, + { + "query": "SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.status = 'pending' AND o.payment_status = 'paid'", + "executionTime": 1856.3, + "calls": 890, + "meanTime": 2.08, + "maxTime": 38.7, + "rows": 5200, + "severity": "HIGH", + "pattern": "MISSING_INDEX", + "suggestion": "Create composite index on orders(status, payment_status, customer_id)" + }, + { + "query": "SELECT p.*, AVG(r.rating) FROM products p LEFT JOIN product_reviews r ON p.id = r.product_id GROUP BY p.id", + "executionTime": 1245.8, + "calls": 2100, + "meanTime": 0.59, + "maxTime": 12.3, + "rows": 100, + "severity": "MEDIUM", + "pattern": "AGGREGATION", + "suggestion": "Consider materialized view for frequently accessed product ratings" + }, + { + "query": "SELECT * FROM audit_log WHERE table_name = 'orders' ORDER BY changed_at DESC LIMIT 1000", + "executionTime": 3456.2, + "calls": 450, + "meanTime": 7.68, + "maxTime": 89.4, + "rows": 1000, + "severity": "CRITICAL", + "pattern": "LARGE_TABLE_SCAN", + "suggestion": "Create composite index on audit_log(table_name, changed_at DESC)" + } + ], + "summary": { + "totalSlowQueries": 4, + "criticalCount": 1, + "highCount": 2, + "mediumCount": 1, + "lowCount": 0, + "totalDatabaseTimeMs": 8898.8 + } +} +SQJSON +) + + # Escape for SQL + slow_query_escaped="${slow_query_json//\'/\'\'}" + + compose exec -T postgres psql -U postgres -d dba_agent -v ON_ERROR_STOP=1 </dev/null || echo "")" + + if [[ -n "$admin_id" ]]; then + sample_transcript=$(cat <<'TRANSCRIPT' +[ + { + "role": "user", + "content": "What are the top 5 most ordered products in the last 30 days?" + }, + { + "role": "assistant", + "content": "I'll query the orders and order_items tables to find the most popular products.\n\nBased on the data, the top 5 most ordered products in the last 30 days are:\n\n1. **ProPhone X15** (SKU: ELEC-SP-002) - 156 orders, $155,843.44 revenue\n2. **AirPods Pro 3** (SKU: ELEC-AU-001) - 142 orders, $39,758.58 revenue\n3. **Budget Phone A5** (SKU: ELEC-SP-007) - 128 orders, $25,598.72 revenue\n4. **Atomic Habits** (SKU: BOOK-002) - 119 orders, $2,260.81 revenue\n5. **Air Fryer XL** (SKU: HOME-KT-005) - 98 orders, $12,739.02 revenue\n\nNotably, electronics dominate the top sellers, with smartphones and audio accessories being particularly popular." + }, + { + "role": "user", + "content": "Can you suggest indexes to improve query performance?" + }, + { + "role": "assistant", + "content": "Based on my analysis of your slow queries and access patterns, here are the recommended indexes:\n\n**High Priority:**\n1. `CREATE INDEX idx_orders_status_payment_created ON orders(status, payment_status, created_at DESC);`\n - Impact: 85% improvement for order filtering queries\n - Affects: 12 slow queries\n\n2. `CREATE INDEX idx_audit_log_table_changed ON audit_log(table_name, changed_at DESC);`\n - Impact: 90% improvement for audit log access\n - Current queries scan 50K+ rows\n\n**Medium Priority:**\n3. `CREATE INDEX idx_reviews_product_approved ON product_reviews(product_id, is_approved) WHERE is_approved = true;`\n - Partial index for approved reviews only\n - Improves product page load times\n\nWould you like me to generate the full DDL statements or analyze any specific queries?" + } +] +TRANSCRIPT +) + # Escape for SQL + transcript_escaped="${sample_transcript//\'/\'\'}" + + compose exec -T postgres psql -U postgres -d dba_agent -v ON_ERROR_STOP=1 </dev/null || echo "{}")" + current_stage="$(printf '%s' "$init_status" | sed -n 's/.*"currentStage":"\([^"]*\)".*/\1/p')" + + if [[ "$current_stage" == "COMPLETED" ]]; then + echo " Brain initialization already completed." + elif [[ "$current_stage" == "FAILED" ]]; then + echo " Note: Brain initialization previously failed. Check LLM configuration." + else + echo " Brain initialization is in progress or pending." + echo " Stage: ${current_stage:-UNKNOWN}" + echo " The initialization will continue in the background." + echo " Run 'curl http://localhost:${DEEPSQL_BACKEND_PORT}/api/connections/${connection_id}/init-status' to check progress." + fi +else + echo " Skipping brain init check (no connection ID available)" +fi + +# ============================================================================ +# Summary +# ============================================================================ + +echo "" +echo "==========================================" +echo "Demo Data Seeding Complete!" +echo "==========================================" +echo "" +echo "What was created:" +echo " - demo_shop database with e-commerce schema" +echo " - Sample products, customers, orders (5000+)" +echo " - Demo connection: ${DEEPSQL_SEED_CONNECTION_NAME}" +if [[ -n "${connection_id:-}" ]]; then +echo " - Connection ID: ${connection_id}" +fi +echo " - Saved queries in SQL Editor" +echo " - Sample slow query analysis" +echo " - Index recommendations" +echo " - Performance actions" +echo " - Sample agent conversation" +echo "" +echo "Demo users (create manually if API creation failed):" +echo " - analyst@demo.local / analyst123!" +echo " - developer@demo.local / developer123!" +echo " - viewer@demo.local / viewer123!" +echo "" +echo "Next steps:" +echo " 1. Open http://localhost:${DEEPSQL_FRONTEND_PORT:-3000}" +echo " 2. Select '${DEEPSQL_SEED_CONNECTION_NAME}' connection" +echo " 3. Explore the Schema, Slow Queries, and Actions tabs" +echo " 4. Try the SQL Editor with pre-saved queries" +echo " 5. Ask the Agent about the database" +echo ""