Skip to content

Workshop Exercise Solutions

This document contains complete solutions for all exercises from the PostgREST workshop modules.

PostgreSQL Introduction Solutions

Exercise 1: Basic Table Operations

-- Create instructors table with constraints
CREATE TABLE instructors (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    specialization VARCHAR(20) CHECK (specialization IN ('database', 'web', 'mobile', 'data-science')),
    years_experience INTEGER CHECK (years_experience > 0),
    hourly_rate DECIMAL(8,2) CHECK (hourly_rate >= 50),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Insert sample data
INSERT INTO instructors (name, email, specialization, years_experience, hourly_rate) VALUES
('John Smith', 'john@example.com', 'database', 8, 120.00),
('Sarah Johnson', 'sarah@example.com', 'web', 5, 95.00),
('Mike Chen', 'mike@example.com', 'data-science', 6, 110.00);

Exercise 2: JSONB Practice

-- Create workshop_feedback table
CREATE TABLE workshop_feedback (
    id SERIAL PRIMARY KEY,
    workshop_id INTEGER REFERENCES workshops(id),
    participant_email VARCHAR(255),
    feedback JSONB
);

-- Insert sample feedback
INSERT INTO workshop_feedback (workshop_id, participant_email, feedback) VALUES
(1, 'alice@example.com', '{"rating": 5, "comments": "Excellent workshop!", "suggestions": ["more hands-on exercises", "longer duration"]}'),
(1, 'bob@example.com', '{"rating": 4, "comments": "Very informative", "suggestions": ["better examples"]}'),
(2, 'carol@example.com', '{"rating": 5, "comments": "Great instructor", "suggestions": ["PostgREST advanced topics"]}');

-- 1. Find all feedback with rating > 4
SELECT * FROM workshop_feedback 
WHERE (feedback->>'rating')::integer > 4;

-- 2. Get average rating per workshop
SELECT 
    w.title,
    ROUND(AVG((f.feedback->>'rating')::integer), 2) as avg_rating
FROM workshop_feedback f
JOIN workshops w ON f.workshop_id = w.id
GROUP BY w.id, w.title;

-- 3. Find workshops mentioned in suggestions
SELECT DISTINCT w.title
FROM workshop_feedback f
JOIN workshops w ON f.workshop_id = w.id
WHERE f.feedback->'suggestions' @> '["PostgREST advanced topics"]';

Exercise 3: Advanced Functions

CREATE OR REPLACE FUNCTION get_instructor_workload(instructor_name VARCHAR)
RETURNS TABLE(
    total_workshops BIGINT,
    total_hours NUMERIC,
    avg_rating NUMERIC,
    total_revenue NUMERIC
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        COUNT(w.id)::BIGINT,
        ROUND(SUM(w.duration) / 60.0, 2),
        ROUND(AVG((f.feedback->>'rating')::integer), 2),
        ROUND(SUM(w.price * COALESCE(pc.count, 0)), 2)
    FROM workshops w
    LEFT JOIN workshop_feedback f ON w.id = f.workshop_id
    LEFT JOIN (
        SELECT workshop_id, COUNT(*) as count
        FROM participants
        GROUP BY workshop_id
    ) pc ON w.id = pc.workshop_id
    WHERE w.instructor = instructor_name;
END;
$$ LANGUAGE plpgsql;

-- Test the function
SELECT * FROM get_instructor_workload('John Doe');

Exercise 4: Views and Analytics

CREATE MATERIALIZED VIEW monthly_workshop_stats AS
SELECT 
    DATE_TRUNC('month', created_at) as month_year,
    COUNT(*) as workshop_count,
    SUM(COALESCE(pc.participant_count, 0)) as total_participants,
    SUM(price * COALESCE(pc.participant_count, 0)) as revenue,
    MODE() WITHIN GROUP (ORDER BY category) as most_popular_category
FROM workshops w
LEFT JOIN (
    SELECT workshop_id, COUNT(*) as participant_count
    FROM participants
    GROUP BY workshop_id
) pc ON w.id = pc.workshop_id
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month_year;

-- Create index
CREATE INDEX idx_monthly_stats_month ON monthly_workshop_stats(month_year);

-- Query the view
SELECT * FROM monthly_workshop_stats;

Exercise 5: ACID Transaction

BEGIN;

-- 1. Create a new workshop
INSERT INTO workshops (title, description, duration, instructor, price, category)
VALUES ('Advanced SQL', 'Deep dive into SQL optimization', 240, 'John Smith', 399.99, 'technical')
RETURNING id AS workshop_id \gset

-- 2. Assign instructor (update instructor record)
UPDATE instructors 
SET hourly_rate = hourly_rate * 1.1 
WHERE name = 'John Smith';

-- 3. Register 3 participants
INSERT INTO participants (name, email, workshop_id) VALUES
('Participant 1', 'p1@example.com', :workshop_id),
('Participant 2', 'p2@example.com', :workshop_id),
('Participant 3', 'p3@example.com', :workshop_id);

-- 4. Update instructor's workshop count (add column if needed)
ALTER TABLE instructors ADD COLUMN IF NOT EXISTS workshop_count INTEGER DEFAULT 0;
UPDATE instructors 
SET workshop_count = workshop_count + 1 
WHERE name = 'John Smith';

-- Verify all operations
SELECT 'Workshop created' as status, COUNT(*) as count FROM workshops WHERE id = :workshop_id
UNION ALL
SELECT 'Participants registered', COUNT(*) FROM participants WHERE workshop_id = :workshop_id
UNION ALL
SELECT 'Instructor updated', workshop_count FROM instructors WHERE name = 'John Smith';

COMMIT;

PostGIS Introduction Solutions

Exercise 1: Basic Spatial Data Creation

-- Create restaurants table with spatial data
CREATE TABLE restaurants (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    cuisine_type VARCHAR(50),
    rating DECIMAL(2,1) CHECK (rating >= 1 AND rating <= 5),
    location GEOMETRY(POINT, 4326),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Create spatial index
CREATE INDEX idx_restaurants_location ON restaurants USING GIST(location);

-- Insert 5 restaurants in Boston area
INSERT INTO restaurants (name, cuisine_type, rating, location) VALUES
('North End Pizza', 'Italian', 4.5, ST_SetSRID(ST_MakePoint(-71.0547, 42.3647), 4326)),
('Chinatown Express', 'Chinese', 4.2, ST_SetSRID(ST_MakePoint(-71.0603, 42.3505), 4326)),
('Back Bay Bistro', 'French', 4.8, ST_SetSRID(ST_MakePoint(-71.0742, 42.3505), 4326)),
('Cambridge Cafe', 'American', 4.0, ST_SetSRID(ST_MakePoint(-71.1190, 42.3736), 4326)),
('Fenway Grill', 'American', 4.3, ST_SetSRID(ST_MakePoint(-71.0972, 42.3467), 4326));

Exercise 2: Distance and Proximity Analysis

-- 1. Find all museums within 2km of Harvard University
SELECT 
    l.name,
    l.category,
    ROUND(ST_Distance(
        l.location::geography,
        (SELECT location FROM locations WHERE name = 'Harvard University')::geography
    )::numeric, 2) as distance_meters
FROM locations l
WHERE l.category = 'museum'
AND ST_DWithin(
    l.location::geography,
    (SELECT location FROM locations WHERE name = 'Harvard University')::geography,
    2000
);

-- 2. Calculate walking distance between all educational institutions
SELECT 
    l1.name as institution1,
    l2.name as institution2,
    ROUND(ST_Distance(l1.location::geography, l2.location::geography)::numeric, 2) as distance_meters
FROM locations l1
CROSS JOIN locations l2
WHERE l1.category = 'education' 
AND l2.category = 'education'
AND l1.id < l2.id;

-- 3. Find closest restaurant to each university
SELECT DISTINCT ON (l.id)
    l.name as university,
    r.name as closest_restaurant,
    r.cuisine_type,
    ROUND(ST_Distance(l.location::geography, r.location::geography)::numeric, 2) as distance_meters
FROM locations l
CROSS JOIN restaurants r
WHERE l.category = 'education'
ORDER BY l.id, ST_Distance(l.location::geography, r.location::geography);

Exercise 3: Spatial Relationships

-- Create neighborhoods table
CREATE TABLE neighborhoods (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    boundary GEOMETRY(POLYGON, 4326)
);

-- Define 3 Boston neighborhoods as polygons
INSERT INTO neighborhoods (name, boundary) VALUES
('Back Bay', ST_SetSRID(ST_GeomFromText('POLYGON((-71.08 42.34, -71.08 42.36, -71.06 42.36, -71.06 42.34, -71.08 42.34))'), 4326)),
('North End', ST_SetSRID(ST_GeomFromText('POLYGON((-71.06 42.36, -71.06 42.37, -71.04 42.37, -71.04 42.36, -71.06 42.36))'), 4326)),
('Cambridge', ST_SetSRID(ST_GeomFromText('POLYGON((-71.16 42.35, -71.16 42.41, -71.06 42.41, -71.06 42.35, -71.16 42.35))'), 4326));

-- Find which locations fall within each neighborhood
SELECT 
    n.name as neighborhood,
    l.name as location_name,
    l.category
FROM neighborhoods n
JOIN locations l ON ST_Within(l.location, n.boundary);

-- Calculate density of locations per neighborhood
SELECT 
    n.name as neighborhood,
    COUNT(l.id) as location_count,
    ROUND(ST_Area(n.boundary::geography) / 1000000, 2) as area_sqkm,
    ROUND(COUNT(l.id) / (ST_Area(n.boundary::geography) / 1000000), 2) as density_per_sqkm
FROM neighborhoods n
LEFT JOIN locations l ON ST_Within(l.location, n.boundary)
GROUP BY n.id, n.name, n.boundary;

Exercise 4: Buffer Analysis

-- Create subway stations (simplified)
CREATE TABLE subway_stations (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    line VARCHAR(50),
    location GEOMETRY(POINT, 4326)
);

INSERT INTO subway_stations (name, line, location) VALUES
('Park Street', 'Red/Green', ST_SetSRID(ST_MakePoint(-71.0624, 42.3560), 4326)),
('Harvard Square', 'Red', ST_SetSRID(ST_MakePoint(-71.1190, 42.3736), 4326)),
('MIT/Kendall', 'Red', ST_SetSRID(ST_MakePoint(-71.0862, 42.3625), 4326));

-- 1. Create 500m and 1km buffers around subway stations
WITH station_buffers AS (
    SELECT 
        name,
        ST_Buffer(location::geography, 500)::geometry as buffer_500m,
        ST_Buffer(location::geography, 1000)::geometry as buffer_1km
    FROM subway_stations
)
SELECT 
    sb.name as station,
    COUNT(CASE WHEN ST_Within(l.location, sb.buffer_500m) THEN 1 END) as locations_500m,
    COUNT(CASE WHEN ST_Within(l.location, sb.buffer_1km) THEN 1 END) as locations_1km
FROM station_buffers sb
CROSS JOIN locations l
GROUP BY sb.name;

-- 2. Find locations within walking distance of public transit
SELECT 
    l.name,
    l.category,
    MIN(ST_Distance(l.location::geography, s.location::geography)) as min_distance_to_transit
FROM locations l
CROSS JOIN subway_stations s
GROUP BY l.id, l.name, l.category
HAVING MIN(ST_Distance(l.location::geography, s.location::geography)) <= 800;

Exercise 5: Route Analysis

-- Create delivery_points table
CREATE TABLE delivery_points (
    id SERIAL PRIMARY KEY,
    address VARCHAR(200),
    location GEOMETRY(POINT, 4326),
    priority INTEGER DEFAULT 1
);

-- Insert 10 delivery locations
INSERT INTO delivery_points (address, location, priority) VALUES
('100 Beacon St', ST_SetSRID(ST_MakePoint(-71.0656, 42.3551), 4326), 1),
('200 Newbury St', ST_SetSRID(ST_MakePoint(-71.0742, 42.3505), 4326), 2),
('MIT Campus', ST_SetSRID(ST_MakePoint(-71.0942, 42.3601), 4326), 1),
('Harvard Square', ST_SetSRID(ST_MakePoint(-71.1190, 42.3736), 4326), 3),
('Fenway Park', ST_SetSRID(ST_MakePoint(-71.0972, 42.3467), 4326), 2),
('North End', ST_SetSRID(ST_MakePoint(-71.0547, 42.3647), 4326), 1),
('South Station', ST_SetSRID(ST_MakePoint(-71.0552, 42.3519), 4326), 3),
('Copley Square', ST_SetSRID(ST_MakePoint(-71.0770, 42.3496), 4326), 2),
('Quincy Market', ST_SetSRID(ST_MakePoint(-71.0546, 42.3601), 4326), 1),
('Back Bay Station', ST_SetSRID(ST_MakePoint(-71.0752, 42.3473), 4326), 2);

-- Calculate distances between all points
WITH point_distances AS (
    SELECT 
        p1.id as from_id,
        p2.id as to_id,
        p1.address as from_address,
        p2.address as to_address,
        ST_Distance(p1.location::geography, p2.location::geography) as distance_meters
    FROM delivery_points p1
    CROSS JOIN delivery_points p2
    WHERE p1.id != p2.id
)
SELECT 
    from_address,
    to_address,
    ROUND(distance_meters::numeric, 2) as distance_meters,
    ROUND((distance_meters / 1000 * 3)::numeric, 1) as estimated_time_minutes -- 20 km/h average
FROM point_distances
ORDER BY distance_meters;

-- Simplified delivery optimization function
CREATE OR REPLACE FUNCTION optimize_delivery_route()
RETURNS TABLE(
    stop_order INTEGER,
    address VARCHAR(200),
    cumulative_distance NUMERIC
) AS $$
DECLARE
    current_point GEOMETRY;
    total_distance NUMERIC := 0;
    stop_count INTEGER := 1;
BEGIN
    -- Start from first point
    SELECT location INTO current_point FROM delivery_points WHERE id = 1;

    RETURN QUERY
    WITH RECURSIVE route_optimization AS (
        -- Base case: start point
        SELECT 1 as stop_order, dp.address, 0::numeric as cumulative_distance, dp.location, ARRAY[dp.id] as visited
        FROM delivery_points dp WHERE id = 1

        UNION ALL

        -- Recursive case: find nearest unvisited point
        SELECT 
            ro.stop_order + 1,
            dp.address,
            ro.cumulative_distance + ST_Distance(ro.location::geography, dp.location::geography),
            dp.location,
            ro.visited || dp.id
        FROM route_optimization ro
        CROSS JOIN delivery_points dp
        WHERE NOT (dp.id = ANY(ro.visited))
        AND ro.stop_order < 10
        ORDER BY ST_Distance(ro.location::geography, dp.location::geography)
        LIMIT 1
    )
    SELECT ro.stop_order, ro.address, ROUND(ro.cumulative_distance::numeric, 2)
    FROM route_optimization ro;
END;
$$ LANGUAGE plpgsql;

Exercise 6: Spatial Aggregation

-- Create spatial dashboard view
CREATE VIEW location_dashboard AS
SELECT 
    l.category,
    r.name as region_name,
    COUNT(l.id) as location_count,
    ROUND(AVG(l.rating), 2) as avg_rating,
    ROUND(ST_Area(ST_ConvexHull(ST_Collect(l.location))::geography) / 1000000, 2) as coverage_area_sqkm,
    ST_AsText(ST_Centroid(ST_Collect(l.location))) as geographic_center
FROM locations l
LEFT JOIN regions r ON ST_Within(l.location, r.boundary)
GROUP BY l.category, r.name
HAVING COUNT(l.id) > 0;

-- Create materialized view for performance
CREATE MATERIALIZED VIEW spatial_analytics_mv AS
SELECT 
    category,
    COUNT(*) as location_count,
    ROUND(ST_Area(ST_ConvexHull(ST_Collect(location))::geography) / 1000000, 2) as coverage_area_sqkm,
    ST_AsText(ST_Centroid(ST_Collect(location))) as category_centroid,
    ROUND(AVG(ST_Distance(
        location::geography,
        ST_SetSRID(ST_MakePoint(-71.0656, 42.3551), 4326)::geography
    ))::numeric, 2) as avg_distance_from_center
FROM locations
GROUP BY category;

CREATE INDEX idx_spatial_analytics_category ON spatial_analytics_mv(category);

-- Query the dashboard
SELECT * FROM location_dashboard ORDER BY location_count DESC;
SELECT * FROM spatial_analytics_mv ORDER BY coverage_area_sqkm DESC;

PostgREST Setup Solutions

Exercise 1: Basic API Operations

# 1. Create a new workshop via POST
curl -X POST "http://localhost:3000/workshops" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "API Testing Workshop",
    "description": "Learn to test APIs effectively",
    "duration": 180,
    "instructor": "Test Instructor"
  }'

# 2. Update workshop duration using PATCH
curl -X PATCH "http://localhost:3000/workshops?id=eq.1" \
  -H "Content-Type: application/json" \
  -d '{"duration": 200}'

# 3. Query workshops by instructor name
curl "http://localhost:3000/workshops?instructor=eq.John%20Doe"

# 4. Delete the workshop created
curl -X DELETE "http://localhost:3000/workshops?title=eq.API%20Testing%20Workshop"

Exercise 2: Advanced Filtering

# 1. Find workshops longer than 2 hours (120 minutes)
curl "http://localhost:3000/workshops?duration=gt.120"

# 2. Get workshops created in the last 7 days
curl "http://localhost:3000/workshops?created_at=gte.$(date -d '7 days ago' -I)"

# 3. Find workshops with "PostgREST" in the title
curl "http://localhost:3000/workshops?title=like.*PostgREST*"

# 4. Get workshops ordered by duration (shortest first)
curl "http://localhost:3000/workshops?order=duration.asc"

Exercise 3: Relationships and Joins

# 1. Get all workshops with their participant count
curl "http://localhost:3000/workshop_stats"

# 2. Find participants registered for "PostgREST Basics"
curl "http://localhost:3000/participants?workshops.title=eq.PostgREST%20Basics&select=*,workshops(title)"

# 3. Get workshops that have more than 1 participant
curl "http://localhost:3000/workshop_stats?participant_count=gt.1"

# 4. List participants with their workshop titles
curl "http://localhost:3000/participants?select=name,email,workshops(title,instructor)"

Exercise 4: Create Custom Views

-- 1. popular_workshops view
CREATE VIEW popular_workshops AS
SELECT 
    w.*,
    COUNT(p.id) as participant_count
FROM workshops w
LEFT JOIN participants p ON w.id = p.workshop_id
GROUP BY w.id
HAVING COUNT(p.id) > 2;

-- 2. instructor_stats view
CREATE VIEW instructor_stats AS
SELECT 
    instructor as instructor_name,
    COUNT(*) as workshop_count,
    SUM(duration) as total_duration_minutes,
    ROUND(AVG(duration), 2) as avg_duration
FROM workshops
GROUP BY instructor;

-- 3. recent_registrations view
CREATE VIEW recent_registrations AS
SELECT 
    p.*,
    w.title as workshop_title
FROM participants p
JOIN workshops w ON p.workshop_id = w.id
WHERE p.registered_at >= CURRENT_DATE - INTERVAL '30 days';

GRANT SELECT ON popular_workshops TO web_anon;
GRANT SELECT ON instructor_stats TO web_anon;
GRANT SELECT ON recent_registrations TO web_anon;
# Test the views
curl "http://localhost:3000/popular_workshops"
curl "http://localhost:3000/instructor_stats"
curl "http://localhost:3000/recent_registrations"

Exercise 5: Performance Testing

-- Create 1000 sample workshops
INSERT INTO workshops (title, description, duration, instructor, price, category)
SELECT 
    'Workshop ' || generate_series,
    'Description for workshop ' || generate_series,
    (RANDOM() * 300 + 60)::INTEGER,
    'Instructor ' || (generate_series % 10 + 1),
    (RANDOM() * 500 + 100)::DECIMAL(8,2),
    CASE (generate_series % 3)
        WHEN 0 THEN 'technical'
        WHEN 1 THEN 'business'
        ELSE 'general'
    END
FROM generate_series(1, 1000);

-- Create indexes
CREATE INDEX idx_workshops_category_perf ON workshops(category);
CREATE INDEX idx_workshops_instructor_perf ON workshops(instructor);
CREATE INDEX idx_workshops_duration_perf ON workshops(duration);
# Test pagination with different page sizes
time curl "http://localhost:3000/workshops?limit=10"
time curl "http://localhost:3000/workshops?limit=100"
time curl "http://localhost:3000/workshops?limit=1000"

# Compare with and without column selection
time curl "http://localhost:3000/workshops"
time curl "http://localhost:3000/workshops?select=id,title,duration"

PostgREST Authentication Solutions

Exercise 1: User Registration and Login

-- Complete authentication setup
CREATE SCHEMA IF NOT EXISTS auth;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pgjwt;

-- Create roles
CREATE ROLE web_anon NOLOGIN;
CREATE ROLE authenticated NOLOGIN;
CREATE ROLE admin_user NOLOGIN;
CREATE ROLE regular_user NOLOGIN;
CREATE ROLE authenticator NOINHERIT LOGIN PASSWORD 'workshop_auth_pass';

GRANT web_anon TO authenticator;
GRANT authenticated TO authenticator;
GRANT admin_user TO authenticator;
GRANT regular_user TO authenticator;

-- Create users table
CREATE TABLE auth.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role VARCHAR(50) DEFAULT 'regular_user',
    active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Password functions
CREATE OR REPLACE FUNCTION auth.hash_password(password TEXT)
RETURNS TEXT AS $$
BEGIN
    RETURN crypt(password, gen_salt('bf', 8));
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE OR REPLACE FUNCTION auth.verify_password(password TEXT, hash TEXT)
RETURNS BOOLEAN AS $$
BEGIN
    RETURN hash = crypt(password, hash);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Registration function
CREATE OR REPLACE FUNCTION auth.register_user(
    email TEXT,
    password TEXT,
    user_role TEXT DEFAULT 'regular_user'
)
RETURNS JSON AS $$
DECLARE
    new_user auth.users;
BEGIN
    IF EXISTS (SELECT 1 FROM auth.users WHERE auth.users.email = register_user.email) THEN
        RETURN json_build_object('error', 'Email already exists');
    END IF;

    INSERT INTO auth.users (email, password_hash, role)
    VALUES (email, auth.hash_password(password), user_role)
    RETURNING * INTO new_user;

    RETURN json_build_object(
        'success', true,
        'user_id', new_user.id,
        'email', new_user.email,
        'role', new_user.role
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Login function
CREATE OR REPLACE FUNCTION auth.login(email TEXT, password TEXT)
RETURNS JSON AS $$
DECLARE
    user_record auth.users;
    jwt_token TEXT;
BEGIN
    SELECT * INTO user_record
    FROM auth.users
    WHERE auth.users.email = login.email AND active = true;

    IF user_record IS NULL OR NOT auth.verify_password(password, user_record.password_hash) THEN
        RETURN json_build_object('error', 'Invalid credentials');
    END IF;

    jwt_token := sign(
        json_build_object(
            'role', user_record.role,
            'user_id', user_record.id,
            'email', user_record.email,
            'exp', extract(epoch from now() + interval '7 days')
        ),
        'your-super-secret-jwt-key-at-least-32-characters-long'
    );

    RETURN json_build_object(
        'token', jwt_token,
        'user', json_build_object(
            'id', user_record.id,
            'email', user_record.email,
            'role', user_record.role
        )
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Grant permissions
GRANT EXECUTE ON FUNCTION auth.register_user TO web_anon;
GRANT EXECUTE ON FUNCTION auth.login TO web_anon;
# Register a new user
curl -X POST "http://localhost:3000/rpc/register_user" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "securepass123",
    "user_role": "regular_user"
  }'

# Login with the user
curl -X POST "http://localhost:3000/rpc/login" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "securepass123"
  }'

# Test protected endpoint
TOKEN="your-jwt-token-here"
curl "http://localhost:3000/workshops" \
  -H "Authorization: Bearer $TOKEN"

Exercise 2: Role-Based Access Control

-- Add owner_id to workshops
ALTER TABLE workshops ADD COLUMN owner_id INTEGER REFERENCES auth.users(id);

-- Enable RLS
ALTER TABLE workshops ENABLE ROW LEVEL SECURITY;

-- Create RLS policies
CREATE POLICY "workshops_select_all" ON workshops FOR SELECT USING (true);

CREATE POLICY "workshops_insert_auth" ON workshops 
FOR INSERT TO authenticated 
WITH CHECK (owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::integer);

CREATE POLICY "workshops_update_owner" ON workshops 
FOR UPDATE TO authenticated 
USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::integer);

CREATE POLICY "workshops_admin_all" ON workshops 
FOR ALL TO admin_user 
USING (true) WITH CHECK (true);

-- Test with different roles
CREATE OR REPLACE FUNCTION create_test_users()
RETURNS JSON AS $$
BEGIN
    INSERT INTO auth.users (email, password_hash, role) VALUES
    ('admin@test.com', auth.hash_password('admin123'), 'admin_user'),
    ('editor@test.com', auth.hash_password('editor123'), 'authenticated'),
    ('viewer@test.com', auth.hash_password('viewer123'), 'regular_user');

    RETURN json_build_object('success', true);
END;
$$ LANGUAGE plpgsql;

SELECT create_test_users();

Exercise 3: JWT Token Management

-- Refresh token function
CREATE TABLE auth.refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id INTEGER REFERENCES auth.users(id) ON DELETE CASCADE,
    expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '30 days',
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION auth.refresh_token(refresh_token_id UUID)
RETURNS JSON AS $$
DECLARE
    token_record auth.refresh_tokens;
    user_record auth.users;
    new_jwt TEXT;
BEGIN
    SELECT * INTO token_record
    FROM auth.refresh_tokens
    WHERE id = refresh_token_id AND expires_at > NOW();

    IF token_record IS NULL THEN
        RETURN json_build_object('error', 'Invalid or expired refresh token');
    END IF;

    SELECT * INTO user_record
    FROM auth.users
    WHERE id = token_record.user_id AND active = true;

    new_jwt := sign(
        json_build_object(
            'role', user_record.role,
            'user_id', user_record.id,
            'email', user_record.email,
            'exp', extract(epoch from now() + interval '7 days')
        ),
        'your-super-secret-jwt-key-at-least-32-characters-long'
    );

    RETURN json_build_object('token', new_jwt);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- User profile management
CREATE OR REPLACE FUNCTION auth.update_profile(
    new_email TEXT DEFAULT NULL,
    new_password TEXT DEFAULT NULL
)
RETURNS JSON AS $$
DECLARE
    current_user_id INTEGER;
BEGIN
    current_user_id := (current_setting('request.jwt.claims', true)::json->>'user_id')::integer;

    IF current_user_id IS NULL THEN
        RETURN json_build_object('error', 'Authentication required');
    END IF;

    UPDATE auth.users
    SET 
        email = COALESCE(new_email, email),
        password_hash = CASE 
            WHEN new_password IS NOT NULL THEN auth.hash_password(new_password)
            ELSE password_hash
        END,
        updated_at = NOW()
    WHERE id = current_user_id;

    RETURN json_build_object('success', true, 'message', 'Profile updated');
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION auth.refresh_token TO web_anon;
GRANT EXECUTE ON FUNCTION auth.update_profile TO authenticated;

PostgREST Functions Solutions

Exercise 1: Basic Function Creation

-- 1. Workshop summary function
CREATE OR REPLACE FUNCTION get_workshop_summary()
RETURNS JSON AS $$
BEGIN
    RETURN (
        SELECT json_build_object(
            'total_workshops', COUNT(w.id),
            'total_participants', COUNT(p.id),
            'average_rating', ROUND(AVG((f.feedback->>'rating')::numeric), 2),
            'generated_at', NOW()
        )
        FROM workshops w
        LEFT JOIN participants p ON w.id = p.workshop_id
        LEFT JOIN workshop_feedback f ON w.id = f.workshop_id
    );
END;
$$ LANGUAGE plpgsql;

-- 2. Keyword search function
CREATE OR REPLACE FUNCTION find_workshops_by_keyword(keyword TEXT)
RETURNS TABLE(
    id INTEGER,
    title VARCHAR(200),
    description TEXT,
    instructor VARCHAR(100),
    relevance REAL
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        w.id,
        w.title,
        w.description,
        w.instructor,
        ts_rank(
            to_tsvector('english', w.title || ' ' || COALESCE(w.description, '') || ' ' || w.instructor),
            plainto_tsquery('english', keyword)
        ) as relevance
    FROM workshops w
    WHERE to_tsvector('english', w.title || ' ' || COALESCE(w.description, '') || ' ' || w.instructor) 
          @@ plainto_tsquery('english', keyword)
    ORDER BY relevance DESC;
END;
$$ LANGUAGE plpgsql;

GRANT EXECUTE ON FUNCTION get_workshop_summary TO web_anon;
GRANT EXECUTE ON FUNCTION find_workshops_by_keyword TO web_anon;
# Test the functions
curl -X POST "http://localhost:3000/rpc/get_workshop_summary"
curl -X POST "http://localhost:3000/rpc/find_workshops_by_keyword" \
  -H "Content-Type: application/json" \
  -d '{"keyword": "PostgREST"}'

Exercise 2: Table-Returning Functions

-- 1. Popular workshops function
CREATE OR REPLACE FUNCTION get_popular_workshops(min_participants INTEGER DEFAULT 1)
RETURNS TABLE(
    workshop_id INTEGER,
    title VARCHAR(200),
    instructor VARCHAR(100),
    participant_count BIGINT,
    duration INTEGER
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        w.id,
        w.title,
        w.instructor,
        COUNT(p.id) as participant_count,
        w.duration
    FROM workshops w
    LEFT JOIN participants p ON w.id = p.workshop_id
    GROUP BY w.id, w.title, w.instructor, w.duration
    HAVING COUNT(p.id) >= min_participants
    ORDER BY COUNT(p.id) DESC;
END;
$$ LANGUAGE plpgsql;

-- 2. Instructor schedule function
CREATE OR REPLACE FUNCTION get_instructor_schedule(instructor_name TEXT)
RETURNS TABLE(
    workshop_title VARCHAR(200),
    duration INTEGER,
    participant_count BIGINT,
    created_at TIMESTAMP
) AS $$
BEGIN
    IF instructor_name IS NULL OR LENGTH(TRIM(instructor_name)) = 0 THEN
        RAISE EXCEPTION 'Instructor name is required';
    END IF;

    RETURN QUERY
    SELECT 
        w.title,
        w.duration,
        COUNT(p.id) as participant_count,
        w.created_at
    FROM workshops w
    LEFT JOIN participants p ON w.id = p.workshop_id
    WHERE w.instructor = instructor_name
    GROUP BY w.id, w.title, w.duration, w.created_at
    ORDER BY w.created_at DESC;
END;
$$ LANGUAGE plpgsql;

GRANT EXECUTE ON FUNCTION get_popular_workshops TO web_anon;
GRANT EXECUTE ON FUNCTION get_instructor_schedule TO web_anon;

Exercise 3: Authentication-Aware Functions

-- 1. User's workshop history
CREATE OR REPLACE FUNCTION my_workshop_history()
RETURNS TABLE(
    workshop_title VARCHAR(200),
    instructor VARCHAR(100),
    duration INTEGER,
    registered_at TIMESTAMP
) AS $$
DECLARE
    current_user_id INTEGER;
BEGIN
    current_user_id := (current_setting('request.jwt.claims', true)::json->>'user_id')::integer;

    IF current_user_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    RETURN QUERY
    SELECT 
        w.title,
        w.instructor,
        w.duration,
        p.registered_at
    FROM participants p
    JOIN workshops w ON p.workshop_id = w.id
    WHERE p.user_id = current_user_id
    ORDER BY p.registered_at DESC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 2. Register for workshop function
CREATE OR REPLACE FUNCTION register_for_workshop(workshop_id INTEGER)
RETURNS JSON AS $$
DECLARE
    current_user_id INTEGER;
    user_email TEXT;
    user_name TEXT;
BEGIN
    current_user_id := (current_setting('request.jwt.claims', true)::json->>'user_id')::integer;

    IF current_user_id IS NULL THEN
        RETURN json_build_object('error', 'Authentication required');
    END IF;

    -- Get user details
    SELECT email INTO user_email FROM auth.users WHERE id = current_user_id;
    user_name := SPLIT_PART(user_email, '@', 1);

    -- Check if already registered
    IF EXISTS(SELECT 1 FROM participants WHERE workshop_id = register_for_workshop.workshop_id AND user_id = current_user_id) THEN
        RETURN json_build_object('error', 'Already registered for this workshop');
    END IF;

    -- Register
    INSERT INTO participants (name, email, workshop_id, user_id)
    VALUES (user_name, user_email, workshop_id, current_user_id);

    RETURN json_build_object('success', true, 'message', 'Successfully registered');
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 3. Admin user statistics
CREATE OR REPLACE FUNCTION get_user_statistics()
RETURNS JSON AS $$
DECLARE
    current_user_role TEXT;
BEGIN
    current_user_role := current_setting('request.jwt.claims', true)::json->>'role';

    IF current_user_role != 'admin_user' THEN
        RAISE EXCEPTION 'Admin access required';
    END IF;

    RETURN (
        SELECT json_build_object(
            'total_users', COUNT(u.id),
            'active_users', COUNT(CASE WHEN u.active THEN 1 END),
            'total_registrations', COUNT(p.id),
            'registrations_last_30_days', COUNT(CASE WHEN p.registered_at > NOW() - INTERVAL '30 days' THEN 1 END)
        )
        FROM auth.users u
        LEFT JOIN participants p ON u.id = p.user_id
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION my_workshop_history TO authenticated;
GRANT EXECUTE ON FUNCTION register_for_workshop TO authenticated;
GRANT EXECUTE ON FUNCTION get_user_statistics TO admin_user;

PostgREST GIS Solutions

Exercise 1: Spatial Data Setup

-- Create restaurants table with spatial data
CREATE TABLE restaurants (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    cuisine VARCHAR(50),
    rating DECIMAL(2,1) CHECK (rating >= 1 AND rating <= 5),
    location GEOMETRY(POINT, 4326),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Create spatial index
CREATE INDEX idx_restaurants_location ON restaurants USING GIST(location);

-- Insert 10 restaurants in Boston area
INSERT INTO restaurants (name, cuisine, rating, location) VALUES
('North End Pizza', 'Italian', 4.5, ST_SetSRID(ST_MakePoint(-71.0547, 42.3647), 4326)),
('Chinatown Express', 'Chinese', 4.2, ST_SetSRID(ST_MakePoint(-71.0603, 42.3505), 4326)),
('Back Bay Bistro', 'French', 4.8, ST_SetSRID(ST_MakePoint(-71.0742, 42.3505), 4326)),
('Cambridge Cafe', 'American', 4.0, ST_SetSRID(ST_MakePoint(-71.1190, 42.3736), 4326)),
('Fenway Grill', 'American', 4.3, ST_SetSRID(ST_MakePoint(-71.0972, 42.3467), 4326)),
('South End Sushi', 'Japanese', 4.6, ST_SetSRID(ST_MakePoint(-71.0723, 42.3467), 4326)),
('Beacon Hill Bakery', 'Bakery', 4.1, ST_SetSRID(ST_MakePoint(-71.0656, 42.3584), 4326)),
('Harvard Square Deli', 'Deli', 3.9, ST_SetSRID(ST_MakePoint(-71.1190, 42.3736), 4326)),
('MIT Food Truck', 'Food Truck', 4.4, ST_SetSRID(ST_MakePoint(-71.0942, 42.3601), 4326)),
('Downtown Donuts', 'Bakery', 3.8, ST_SetSRID(ST_MakePoint(-71.0589, 42.3581), 4326));

-- Grant permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON restaurants TO web_anon;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO web_anon;
# Test basic spatial queries
curl "http://localhost:3000/restaurants?select=id,name,cuisine,rating,location"
curl "http://localhost:3000/restaurants?cuisine=eq.Italian"

Exercise 2: Distance-Based Functions

-- 1. Find restaurants nearby with cuisine filter
CREATE OR REPLACE FUNCTION find_restaurants_nearby(
    lat DECIMAL,
    lng DECIMAL,
    radius INTEGER DEFAULT 1000,
    cuisine_type TEXT DEFAULT NULL
)
RETURNS TABLE(
    id INTEGER,
    name VARCHAR(100),
    cuisine VARCHAR(50),
    rating DECIMAL,
    distance_meters DECIMAL,
    geometry JSON
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        r.id,
        r.name,
        r.cuisine,
        r.rating,
        ROUND(ST_Distance(
            r.location::geography,
            ST_SetSRID(ST_MakePoint(lng, lat), 4326)::geography
        )::numeric, 2) as distance_meters,
        ST_AsGeoJSON(r.location)::json as geometry
    FROM restaurants r
    WHERE ST_DWithin(
        r.location::geography,
        ST_SetSRID(ST_MakePoint(lng, lat), 4326)::geography,
        radius
    )
    AND (cuisine_type IS NULL OR r.cuisine = cuisine_type)
    ORDER BY distance_meters;
END;
$$ LANGUAGE plpgsql;

-- 2. Restaurant density by region
CREATE OR REPLACE FUNCTION get_restaurant_density(region_name TEXT)
RETURNS JSON AS $$
DECLARE
    region_geom GEOMETRY;
    result JSON;
BEGIN
    SELECT boundary INTO region_geom
    FROM regions
    WHERE name = region_name;

    IF region_geom IS NULL THEN
        RETURN json_build_object('error', 'Region not found');
    END IF;

    SELECT json_build_object(
        'region_name', region_name,
        'total_restaurants', COUNT(r.id),
        'restaurants_by_cuisine', json_object_agg(r.cuisine, cuisine_count),
        'average_rating', ROUND(AVG(r.rating), 2),
        'area_sqkm', (SELECT area_sqkm FROM regions WHERE name = region_name),
        'density_per_sqkm', ROUND(COUNT(r.id)::decimal / (SELECT area_sqkm FROM regions WHERE name = region_name), 2)
    ) INTO result
    FROM (
        SELECT 
            r.cuisine,
            COUNT(*) as cuisine_count,
            AVG(r.rating) as avg_rating
        FROM restaurants r
        WHERE ST_Within(r.location, region_geom)
        GROUP BY r.cuisine
    ) r;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- 3. Find food deserts
CREATE OR REPLACE FUNCTION find_food_deserts(max_distance INTEGER DEFAULT 1000)
RETURNS TABLE(
    grid_id INTEGER,
    center_point JSON,
    nearest_restaurant_distance DECIMAL
) AS $$
BEGIN
    RETURN QUERY
    WITH grid_points AS (
        SELECT 
            ROW_NUMBER() OVER() as id,
            ST_SetSRID(ST_MakePoint(
                -71.2 + (x * 0.01),
                42.2 + (y * 0.01)
            ), 4326) as point
        FROM generate_series(0, 20) x
        CROSS JOIN generate_series(0, 20) y
    )
    SELECT 
        gp.id::INTEGER,
        ST_AsGeoJSON(gp.point)::json,
        ROUND(MIN(ST_Distance(gp.point::geography, r.location::geography))::numeric, 2)
    FROM grid_points gp
    CROSS JOIN restaurants r
    GROUP BY gp.id, gp.point
    HAVING MIN(ST_Distance(gp.point::geography, r.location::geography)) > max_distance
    ORDER BY MIN(ST_Distance(gp.point::geography, r.location::geography)) DESC;
END;
$$ LANGUAGE plpgsql;

GRANT EXECUTE ON FUNCTION find_restaurants_nearby TO web_anon;
GRANT EXECUTE ON FUNCTION get_restaurant_density TO web_anon;
GRANT EXECUTE ON FUNCTION find_food_deserts TO web_anon;

Exercise 3: Spatial Analysis API

-- Create delivery zones
CREATE TABLE delivery_zones (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    zone_type VARCHAR(50),
    boundary GEOMETRY(POLYGON, 4326),
    delivery_fee DECIMAL(5,2),
    max_delivery_time INTEGER -- minutes
);

INSERT INTO delivery_zones (name, zone_type, boundary, delivery_fee, max_delivery_time) VALUES
('Downtown Zone', 'premium', 
 ST_SetSRID(ST_GeomFromText('POLYGON((-71.08 42.34, -71.08 42.37, -71.04 42.37, -71.04 42.34, -71.08 42.34))'), 4326),
 2.99, 30),
('Cambridge Zone', 'standard',
 ST_SetSRID(ST_GeomFromText('POLYGON((-71.16 42.35, -71.16 42.41, -71.06 42.41, -71.06 42.35, -71.16 42.35))'), 4326),
 4.99, 45),
('Extended Zone', 'extended',
 ST_SetSRID(ST_GeomFromText('POLYGON((-71.20 42.25, -71.20 42.45, -70.95 42.45, -70.95 42.25, -71.20 42.25))'), 4326),
 7.99, 60);

-- 1. Check delivery availability
CREATE OR REPLACE FUNCTION check_delivery_availability(lat DECIMAL, lng DECIMAL)
RETURNS JSON AS $$
DECLARE
    delivery_point GEOMETRY;
    zone_info RECORD;
BEGIN
    delivery_point := ST_SetSRID(ST_MakePoint(lng, lat), 4326);

    SELECT * INTO zone_info
    FROM delivery_zones
    WHERE ST_Within(delivery_point, boundary)
    ORDER BY delivery_fee ASC
    LIMIT 1;

    IF zone_info IS NULL THEN
        RETURN json_build_object(
            'available', false,
            'message', 'Delivery not available in this area'
        );
    END IF;

    RETURN json_build_object(
        'available', true,
        'zone_name', zone_info.name,
        'delivery_fee', zone_info.delivery_fee,
        'estimated_time', zone_info.max_delivery_time,
        'nearby_restaurants', (
            SELECT json_agg(json_build_object(
                'name', r.name,
                'cuisine', r.cuisine,
                'rating', r.rating,
                'distance_meters', ROUND(ST_Distance(r.location::geography, delivery_point::geography)::numeric, 2)
            ))
            FROM restaurants r
            WHERE ST_DWithin(r.location::geography, delivery_point::geography, 2000)
            ORDER BY ST_Distance(r.location::geography, delivery_point::geography)
            LIMIT 5
        )
    );
END;
$$ LANGUAGE plpgsql;

-- 2. Optimize delivery route
CREATE OR REPLACE FUNCTION optimize_delivery_route(
    restaurant_id INTEGER,
    delivery_addresses JSONB
)
RETURNS JSON AS $$
DECLARE
    restaurant_location GEOMETRY;
    address JSONB;
    route_points GEOMETRY[];
    total_distance DECIMAL := 0;
    route_info JSON;
BEGIN
    -- Get restaurant location
    SELECT location INTO restaurant_location
    FROM restaurants
    WHERE id = restaurant_id;

    IF restaurant_location IS NULL THEN
        RETURN json_build_object('error', 'Restaurant not found');
    END IF;

    -- Build route (simplified nearest neighbor)
    route_points := ARRAY[restaurant_location];

    FOR address IN SELECT * FROM jsonb_array_elements(delivery_addresses)
    LOOP
        route_points := array_append(route_points, 
            ST_SetSRID(ST_MakePoint(
                (address->>'lng')::decimal,
                (address->>'lat')::decimal
            ), 4326)
        );
    END LOOP;

    -- Calculate total distance
    FOR i IN 1..array_length(route_points, 1)-1
    LOOP
        total_distance := total_distance + ST_Distance(
            route_points[i]::geography,
            route_points[i+1]::geography
        );
    END LOOP;

    RETURN json_build_object(
        'restaurant_id', restaurant_id,
        'total_distance_meters', ROUND(total_distance::numeric, 2),
        'estimated_time_minutes', ROUND((total_distance / 1000 * 3)::numeric, 0), -- 20 km/h average
        'delivery_points', jsonb_array_length(delivery_addresses),
        'route_geometry', ST_AsGeoJSON(ST_MakeLine(route_points))::json
    );
END;
$$ LANGUAGE plpgsql;

GRANT EXECUTE ON FUNCTION check_delivery_availability TO web_anon;
GRANT EXECUTE ON FUNCTION optimize_delivery_route TO authenticated;
# Test delivery functions
curl -X POST "http://localhost:3000/rpc/check_delivery_availability" \
  -H "Content-Type: application/json" \
  -d '{"lat": 42.3551, "lng": -71.0656}'

curl -X POST "http://localhost:3000/rpc/optimize_delivery_route" \
  -H "Content-Type: application/json" \
  -d '{
    "restaurant_id": 1,
    "delivery_addresses": [
      {"lat": 42.3551, "lng": -71.0656, "address": "Boston Common"},
      {"lat": 42.3736, "lng": -71.1190, "address": "Harvard Square"}
    ]
  }'

This comprehensive solutions document provides working code for all exercises across the workshop modules, demonstrating practical implementation of PostgreSQL, PostGIS, and PostgREST concepts with real-world scenarios.