add: tests using an actual database

This commit is contained in:
2026-03-29 15:24:09 -07:00
parent 7626b5618a
commit bddfbd4f74
6 changed files with 295 additions and 8 deletions
+8
View File
@@ -0,0 +1,8 @@
package schema
import (
_ "embed"
)
//go:embed schema.sql
var Bytes []byte
+229
View File
@@ -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;