230 lines
6.6 KiB
PL/PgSQL
230 lines
6.6 KiB
PL/PgSQL
-- 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;
|