00001_initial_schema.sql 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. -- 00001_initial_schema.sql
  2. -- MovieDice: initial database schema
  3. -- Tables: users, groups, group_members, movies, landing_reel_posters
  4. -- =============================================================================
  5. -- users
  6. -- =============================================================================
  7. CREATE TABLE public.users (
  8. id uuid PRIMARY KEY, -- maps to auth.users.id (Supabase Anonymous Sign-In)
  9. display_name text NOT NULL
  10. CONSTRAINT users_display_name_length CHECK (char_length(display_name) BETWEEN 1 AND 30)
  11. CONSTRAINT users_display_name_no_html CHECK (display_name !~ '[<>]')
  12. CONSTRAINT users_display_name_no_control CHECK (display_name !~ '[\x00-\x1F\x7F]'),
  13. avatar_color text
  14. CONSTRAINT users_avatar_color_hex CHECK (avatar_color IS NULL OR avatar_color ~ '^#[0-9a-fA-F]{6}$'),
  15. recovery_code text, -- Argon2id hashed, nullable, single-use
  16. last_active_at timestamptz NOT NULL DEFAULT now(),
  17. created_at timestamptz NOT NULL DEFAULT now()
  18. );
  19. -- =============================================================================
  20. -- groups
  21. -- =============================================================================
  22. CREATE TABLE public.groups (
  23. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  24. name text NOT NULL
  25. CONSTRAINT groups_name_length CHECK (char_length(name) BETWEEN 1 AND 50)
  26. CONSTRAINT groups_name_no_html CHECK (name !~ '[<>]')
  27. CONSTRAINT groups_name_no_control CHECK (name !~ '[\x00-\x1F\x7F]'),
  28. invite_code text NOT NULL UNIQUE,
  29. created_by uuid NOT NULL REFERENCES public.users(id),
  30. created_at timestamptz NOT NULL DEFAULT now()
  31. );
  32. -- Note: invite_code already indexed by the UNIQUE constraint
  33. -- =============================================================================
  34. -- group_members
  35. -- =============================================================================
  36. CREATE TABLE public.group_members (
  37. group_id uuid NOT NULL REFERENCES public.groups(id) ON DELETE CASCADE,
  38. user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
  39. role text NOT NULL DEFAULT 'member'
  40. CONSTRAINT group_members_role_valid CHECK (role IN ('admin', 'member')),
  41. joined_at timestamptz NOT NULL DEFAULT now(),
  42. PRIMARY KEY (group_id, user_id)
  43. );
  44. CREATE INDEX idx_group_members_user_id ON public.group_members (user_id);
  45. -- Note: group_id already indexed as leading column of the composite PK
  46. -- =============================================================================
  47. -- movies
  48. -- =============================================================================
  49. CREATE TABLE public.movies (
  50. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  51. group_id uuid NOT NULL REFERENCES public.groups(id) ON DELETE CASCADE,
  52. tmdb_id integer NOT NULL,
  53. title text NOT NULL,
  54. year integer,
  55. poster_path text,
  56. genres text[],
  57. trailer_url text
  58. CONSTRAINT movies_trailer_url_domain CHECK (
  59. trailer_url IS NULL
  60. OR trailer_url ~ '^https://(www\.)?(youtube\.com|themoviedb\.org|imdb\.com)/'
  61. ),
  62. trailer_url_refreshed_at timestamptz,
  63. metadata_refreshed_at timestamptz,
  64. added_by uuid REFERENCES public.users(id) ON DELETE SET NULL,
  65. watched boolean NOT NULL DEFAULT false,
  66. watched_at timestamptz,
  67. added_at timestamptz NOT NULL DEFAULT now()
  68. );
  69. CREATE INDEX idx_movies_group_id ON public.movies (group_id);
  70. CREATE INDEX idx_movies_added_by ON public.movies (added_by);
  71. -- =============================================================================
  72. -- landing_reel_posters
  73. -- =============================================================================
  74. CREATE TABLE public.landing_reel_posters (
  75. id serial PRIMARY KEY,
  76. tmdb_id integer NOT NULL,
  77. poster_path text NOT NULL,
  78. title text NOT NULL,
  79. refreshed_at timestamptz NOT NULL DEFAULT now()
  80. );