add: tests using an actual database
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
-- Database schema for Webstory
|
||||
-- This schema supports the resources defined in proto/webstory/v1/api.proto
|
||||
|
||||
-- Enable UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Stories table
|
||||
CREATE TABLE IF NOT EXISTS stories (
|
||||
-- Primary key
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Story ID (used in resource name: stories/{story_id})
|
||||
story_id VARCHAR(63) NOT NULL,
|
||||
|
||||
-- Title of the story
|
||||
title VARCHAR(500) NOT NULL,
|
||||
|
||||
-- Content of the story
|
||||
content TEXT,
|
||||
|
||||
-- Description or summary of the story
|
||||
description TEXT,
|
||||
|
||||
-- Labels for organizing and categorizing the story
|
||||
labels TEXT[],
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Etag for concurrency control
|
||||
etag VARCHAR(128) NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT stories_story_id_unique UNIQUE (story_id),
|
||||
CONSTRAINT stories_story_id_pattern CHECK (story_id ~ '^[a-z][a-z0-9-]{2,61}$'),
|
||||
CONSTRAINT stories_etag_pattern CHECK (char_length(etag) > 0)
|
||||
);
|
||||
|
||||
-- Create index for efficient filtering and sorting
|
||||
CREATE INDEX IF NOT EXISTS idx_stories_title ON stories (title);
|
||||
CREATE INDEX IF NOT EXISTS idx_stories_labels ON stories USING GIN (labels);
|
||||
CREATE INDEX IF NOT EXISTS idx_stories_created_at ON stories (created_at DESC);
|
||||
|
||||
-- Scenes table (child of stories)
|
||||
CREATE TABLE IF NOT EXISTS scenes (
|
||||
-- Primary key
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Foreign key to stories
|
||||
story_id INTEGER NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
|
||||
|
||||
-- Scene ID (used in resource name: stories/{story}/scenes/{scene_id})
|
||||
scene_id VARCHAR(63) NOT NULL,
|
||||
|
||||
-- Scene number within the story
|
||||
scene_number INTEGER NOT NULL,
|
||||
|
||||
-- Title of the scene
|
||||
title VARCHAR(500) NOT NULL,
|
||||
|
||||
-- Content of the scene
|
||||
content TEXT,
|
||||
|
||||
-- Description of the scene
|
||||
description TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Etag for concurrency control
|
||||
etag VARCHAR(128) NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT scenes_story_scene_id_unique UNIQUE (story_id, scene_id),
|
||||
CONSTRAINT scenes_story_id_exists CHECK (story_id > 0),
|
||||
CONSTRAINT scenes_scene_id_pattern CHECK (scene_id ~ '^[a-z][0-9-]{2,61}[0-9]$'),
|
||||
CONSTRAINT scenes_scene_number_positive CHECK (scene_number > 0),
|
||||
CONSTRAINT scenes_etag_pattern CHECK (char_length(etag) > 0)
|
||||
);
|
||||
|
||||
-- Create index for efficient queries
|
||||
CREATE INDEX IF NOT EXISTS idx_scenes_story_id ON scenes (story_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_scenes_scene_number ON scenes (story_id, scene_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_scenes_title ON scenes (title);
|
||||
|
||||
-- Actors table (child of stories)
|
||||
CREATE TABLE IF NOT EXISTS actors (
|
||||
-- Primary key
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Foreign key to stories
|
||||
story_id INTEGER NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
|
||||
|
||||
-- Actor ID (used in resource name: stories/{story}/actors/{actor_id})
|
||||
actor_id VARCHAR(63) NOT NULL,
|
||||
|
||||
-- Name of the actor
|
||||
name_value VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Role or character name this actor plays in the story
|
||||
role VARCHAR(255),
|
||||
|
||||
-- Optional notes about the actor
|
||||
notes TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Etag for concurrency control
|
||||
etag VARCHAR(128) NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT actors_story_actor_id_unique UNIQUE (story_id, actor_id),
|
||||
CONSTRAINT actors_story_id_exists CHECK (story_id > 0),
|
||||
CONSTRAINT actors_actor_id_pattern CHECK (actor_id ~ '^[a-z][0-9-]{2,61}[0-9]$'),
|
||||
CONSTRAINT actors_etag_pattern CHECK (char_length(etag) > 0)
|
||||
);
|
||||
|
||||
-- Create index for efficient queries
|
||||
CREATE INDEX IF NOT EXISTS idx_actors_story_id ON actors (story_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_actors_name_value ON actors (story_id, name_value);
|
||||
CREATE INDEX IF NOT EXISTS idx_actors_role ON actors (story_id, role);
|
||||
|
||||
-- Function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
-- Update etag when row is modified (simple version using timestamp)
|
||||
NEW.etag := md5(NEW.created_at::text || NEW.updated_at::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger for stories table
|
||||
CREATE TRIGGER update_stories_updated_at
|
||||
BEFORE UPDATE ON stories
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Trigger for scenes table
|
||||
CREATE TRIGGER update_scenes_updated_at
|
||||
BEFORE UPDATE ON scenes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Trigger for actors table
|
||||
CREATE TRIGGER update_actors_updated_at
|
||||
BEFORE UPDATE ON actors
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Function to generate new etag (called during inserts)
|
||||
CREATE OR REPLACE FUNCTION generate_etag()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.etag := md5(NEW.id::text || NEW.created_at::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger for etag generation on inserts
|
||||
CREATE TRIGGER generate_stories_etag
|
||||
BEFORE INSERT ON stories
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION generate_etag();
|
||||
|
||||
CREATE TRIGGER generate_scenes_etag
|
||||
BEFORE INSERT ON scenes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION generate_etag();
|
||||
|
||||
CREATE TRIGGER generate_actors_etag
|
||||
BEFORE INSERT ON actors
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION generate_etag();
|
||||
|
||||
-- Helper function to get stories with label filtering
|
||||
CREATE OR REPLACE FUNCTION get_stories(
|
||||
p_page_size INTEGER DEFAULT 50,
|
||||
p_page_token TEXT DEFAULT NULL,
|
||||
p_filter TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id INTEGER,
|
||||
story_id VARCHAR(63),
|
||||
title VARCHAR(500),
|
||||
content TEXT,
|
||||
description TEXT,
|
||||
labels TEXT[],
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
etag VARCHAR(128)
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_page_size INTEGER;
|
||||
v_last_id INTEGER;
|
||||
BEGIN
|
||||
-- Validate and limit page size
|
||||
v_page_size := LEAST(GREATEST(p_page_size, 1), 1000);
|
||||
|
||||
-- Parse page token to get last ID for pagination
|
||||
IF p_page_token IS NOT NULL AND p_page_token != '' THEN
|
||||
v_last_id := p_page_token::INTEGER;
|
||||
END IF;
|
||||
|
||||
-- Build dynamic query based on filter
|
||||
-- Note: This is a simplified version. Production code should handle
|
||||
-- filter parsing more robustly
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
s.id,
|
||||
s.story_id,
|
||||
s.title,
|
||||
s.content,
|
||||
s.description,
|
||||
s.labels,
|
||||
s.created_at,
|
||||
s.updated_at,
|
||||
s.etag
|
||||
FROM stories s
|
||||
WHERE (v_last_id IS NULL OR s.id > v_last_id)
|
||||
ORDER BY s.id
|
||||
LIMIT v_page_size;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Reference in New Issue
Block a user