735 lines
21 KiB
PL/PgSQL
735 lines
21 KiB
PL/PgSQL
create extension if not exists pgcrypto;
|
|
|
|
create table if not exists public.wordle_words (
|
|
position integer primary key,
|
|
word text not null unique check (word ~ '^[a-z]{5}$')
|
|
);
|
|
|
|
create table if not exists public.wordle_dictionary (
|
|
word text primary key check (word ~ '^[a-z]{5}$')
|
|
);
|
|
|
|
create table if not exists public.profiles (
|
|
id uuid primary key references auth.users(id) on delete cascade,
|
|
username text,
|
|
display_name text,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
alter table public.profiles
|
|
add column if not exists username text;
|
|
|
|
create unique index if not exists profiles_username_key
|
|
on public.profiles (lower(username))
|
|
where username is not null;
|
|
|
|
create table if not exists public.wordle_rounds (
|
|
id uuid primary key default gen_random_uuid(),
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
word text not null check (word ~ '^[a-z]{5}$'),
|
|
hour_start timestamptz not null,
|
|
started_at timestamptz not null default now(),
|
|
next_playable_at timestamptz not null,
|
|
completed_at timestamptz,
|
|
won boolean,
|
|
guess_count integer check (guess_count between 1 and 6),
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
alter table public.wordle_rounds
|
|
add column if not exists hour_start timestamptz,
|
|
add column if not exists next_playable_at timestamptz;
|
|
|
|
update public.wordle_rounds
|
|
set hour_start = date_trunc('hour', started_at)
|
|
where hour_start is null;
|
|
|
|
update public.wordle_rounds
|
|
set next_playable_at = hour_start + interval '1 hour'
|
|
where next_playable_at is null;
|
|
|
|
alter table public.wordle_rounds
|
|
alter column hour_start set not null,
|
|
alter column next_playable_at set not null;
|
|
|
|
create table if not exists public.wordle_guesses (
|
|
id uuid primary key default gen_random_uuid(),
|
|
round_id uuid not null references public.wordle_rounds(id) on delete cascade,
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
guess text not null check (guess ~ '^[a-z]{5}$'),
|
|
row_index integer not null check (row_index between 1 and 6),
|
|
states jsonb not null,
|
|
created_at timestamptz not null default now(),
|
|
unique (round_id, row_index)
|
|
);
|
|
|
|
create index if not exists wordle_rounds_user_started_idx
|
|
on public.wordle_rounds (user_id, started_at desc);
|
|
|
|
create unique index if not exists wordle_rounds_user_hour_idx
|
|
on public.wordle_rounds (user_id, hour_start);
|
|
|
|
create index if not exists wordle_guesses_round_row_idx
|
|
on public.wordle_guesses (round_id, row_index);
|
|
|
|
alter table public.wordle_words enable row level security;
|
|
alter table public.wordle_dictionary enable row level security;
|
|
alter table public.profiles enable row level security;
|
|
alter table public.wordle_rounds enable row level security;
|
|
alter table public.wordle_guesses enable row level security;
|
|
|
|
drop policy if exists "Users can read their own profile" on public.profiles;
|
|
create policy "Users can read their own profile"
|
|
on public.profiles
|
|
for select
|
|
using (auth.uid() = id);
|
|
|
|
drop policy if exists "Users can update their own profile" on public.profiles;
|
|
create policy "Users can update their own profile"
|
|
on public.profiles
|
|
for update
|
|
using (auth.uid() = id)
|
|
with check (auth.uid() = id);
|
|
|
|
drop policy if exists "Users can read their own rounds" on public.wordle_rounds;
|
|
create policy "Users can read their own rounds"
|
|
on public.wordle_rounds
|
|
for select
|
|
using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "Users can read their own guesses" on public.wordle_guesses;
|
|
create policy "Users can read their own guesses"
|
|
on public.wordle_guesses
|
|
for select
|
|
using (auth.uid() = user_id);
|
|
|
|
create or replace function public.handle_new_user()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
begin
|
|
insert into public.profiles (id, username, display_name)
|
|
values (
|
|
new.id,
|
|
nullif(lower(regexp_replace(coalesce(new.raw_user_meta_data->>'username', split_part(new.email, '@', 1)), '[^a-z0-9_]', '', 'g')), ''),
|
|
coalesce(new.raw_user_meta_data->>'display_name', new.raw_user_meta_data->>'username', split_part(new.email, '@', 1))
|
|
)
|
|
on conflict (id) do nothing;
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
drop trigger if exists on_auth_user_created on auth.users;
|
|
create trigger on_auth_user_created
|
|
after insert on auth.users
|
|
for each row execute function public.handle_new_user();
|
|
|
|
create or replace function public.score_guess(guess text, answer text)
|
|
returns jsonb
|
|
language plpgsql
|
|
immutable
|
|
as $$
|
|
declare
|
|
states text[] := array['wrong', 'wrong', 'wrong', 'wrong', 'wrong'];
|
|
remaining_letters text := '';
|
|
guess_letter text;
|
|
answer_letter text;
|
|
match_index integer;
|
|
index integer;
|
|
begin
|
|
for index in 1..5 loop
|
|
guess_letter := substr(guess, index, 1);
|
|
answer_letter := substr(answer, index, 1);
|
|
|
|
if guess_letter = answer_letter then
|
|
states[index] := 'correct';
|
|
else
|
|
remaining_letters := remaining_letters || answer_letter;
|
|
end if;
|
|
end loop;
|
|
|
|
for index in 1..5 loop
|
|
if states[index] = 'correct' then
|
|
continue;
|
|
end if;
|
|
|
|
guess_letter := substr(guess, index, 1);
|
|
match_index := strpos(remaining_letters, guess_letter);
|
|
|
|
if match_index > 0 then
|
|
states[index] := 'wrong-position';
|
|
remaining_letters := overlay(remaining_letters placing '' from match_index for 1);
|
|
end if;
|
|
end loop;
|
|
|
|
return to_jsonb(states);
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.get_hourly_word(hour_start timestamptz)
|
|
returns text
|
|
language plpgsql
|
|
stable
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
word_count integer;
|
|
word_offset integer;
|
|
hourly_word text;
|
|
begin
|
|
select count(*) into word_count from public.wordle_words;
|
|
|
|
if word_count = 0 then
|
|
raise exception 'No hourly words are configured';
|
|
end if;
|
|
|
|
word_offset := (floor(extract(epoch from hour_start) / 3600)::bigint % word_count)::integer;
|
|
|
|
select public.wordle_words.word
|
|
into hourly_word
|
|
from public.wordle_words
|
|
order by position
|
|
limit 1 offset word_offset;
|
|
|
|
return hourly_word;
|
|
end;
|
|
$$;
|
|
|
|
drop function if exists public.start_hourly_round(text);
|
|
|
|
create or replace function public.start_hourly_round()
|
|
returns table (
|
|
round_id uuid,
|
|
hour_start timestamptz,
|
|
started_at timestamptz,
|
|
next_playable_at timestamptz,
|
|
completed_at timestamptz,
|
|
won boolean,
|
|
guess_count integer,
|
|
server_now timestamptz,
|
|
is_existing boolean,
|
|
revealed_word text,
|
|
guesses jsonb
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
existing_round public.wordle_rounds%rowtype;
|
|
new_round public.wordle_rounds%rowtype;
|
|
current_hour timestamptz := date_trunc('hour', now());
|
|
hourly_word text := public.get_hourly_word(date_trunc('hour', now()));
|
|
begin
|
|
if auth.uid() is null then
|
|
raise exception 'Authentication required';
|
|
end if;
|
|
|
|
select *
|
|
into existing_round
|
|
from public.wordle_rounds
|
|
where user_id = auth.uid()
|
|
and public.wordle_rounds.hour_start = current_hour
|
|
order by started_at desc
|
|
limit 1;
|
|
|
|
if found then
|
|
return query select
|
|
existing_round.id,
|
|
existing_round.hour_start,
|
|
existing_round.started_at,
|
|
existing_round.next_playable_at,
|
|
existing_round.completed_at,
|
|
existing_round.won,
|
|
existing_round.guess_count,
|
|
now(),
|
|
true,
|
|
case when existing_round.completed_at is null then null else existing_round.word end,
|
|
coalesce((
|
|
select jsonb_agg(jsonb_build_object(
|
|
'guess', wordle_guesses.guess,
|
|
'rowIndex', wordle_guesses.row_index,
|
|
'states', wordle_guesses.states
|
|
) order by wordle_guesses.row_index)
|
|
from public.wordle_guesses
|
|
where wordle_guesses.round_id = existing_round.id
|
|
), '[]'::jsonb);
|
|
return;
|
|
end if;
|
|
|
|
insert into public.wordle_rounds (user_id, word, hour_start, next_playable_at)
|
|
values (auth.uid(), hourly_word, current_hour, current_hour + interval '1 hour')
|
|
returning * into new_round;
|
|
|
|
return query select
|
|
new_round.id,
|
|
new_round.hour_start,
|
|
new_round.started_at,
|
|
new_round.next_playable_at,
|
|
new_round.completed_at,
|
|
new_round.won,
|
|
new_round.guess_count,
|
|
now(),
|
|
false,
|
|
null::text,
|
|
'[]'::jsonb;
|
|
end;
|
|
$$;
|
|
|
|
drop function if exists public.submit_guess(uuid, text);
|
|
|
|
create or replace function public.submit_guess(target_round_id uuid, submitted_guess text)
|
|
returns table (
|
|
guess text,
|
|
row_index integer,
|
|
states jsonb,
|
|
completed boolean,
|
|
won boolean,
|
|
guess_count integer,
|
|
revealed_word text,
|
|
next_playable_at timestamptz,
|
|
server_now timestamptz
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
active_round public.wordle_rounds%rowtype;
|
|
normalized_guess text := lower(trim(submitted_guess));
|
|
next_row integer;
|
|
scored_states jsonb;
|
|
did_win boolean;
|
|
did_complete boolean;
|
|
begin
|
|
if auth.uid() is null then
|
|
raise exception 'Authentication required';
|
|
end if;
|
|
|
|
if normalized_guess !~ '^[a-z]{5}$' then
|
|
raise exception 'Guess must be five letters';
|
|
end if;
|
|
|
|
if not exists (select 1 from public.wordle_dictionary where word = normalized_guess) then
|
|
raise exception 'Not in word list';
|
|
end if;
|
|
|
|
select *
|
|
into active_round
|
|
from public.wordle_rounds rounds
|
|
where rounds.id = target_round_id
|
|
and rounds.user_id = auth.uid()
|
|
limit 1;
|
|
|
|
if not found then
|
|
raise exception 'Round not found';
|
|
end if;
|
|
|
|
if active_round.completed_at is not null then
|
|
raise exception 'Round already complete';
|
|
end if;
|
|
|
|
select (count(*) + 1)::integer
|
|
into next_row
|
|
from public.wordle_guesses
|
|
where wordle_guesses.round_id = active_round.id;
|
|
|
|
if next_row > 6 then
|
|
raise exception 'No guesses remaining';
|
|
end if;
|
|
|
|
scored_states := public.score_guess(normalized_guess, active_round.word);
|
|
did_win := normalized_guess = active_round.word;
|
|
did_complete := did_win or next_row = 6;
|
|
|
|
insert into public.wordle_guesses (round_id, user_id, guess, row_index, states)
|
|
values (active_round.id, auth.uid(), normalized_guess, next_row, scored_states);
|
|
|
|
if did_complete then
|
|
update public.wordle_rounds
|
|
set completed_at = now(),
|
|
won = did_win,
|
|
guess_count = next_row
|
|
where id = active_round.id;
|
|
end if;
|
|
|
|
return query select
|
|
normalized_guess,
|
|
next_row,
|
|
scored_states,
|
|
did_complete,
|
|
did_win,
|
|
case when did_complete then next_row else null end,
|
|
case when did_complete then active_round.word else null end,
|
|
active_round.next_playable_at,
|
|
now();
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.get_user_stats()
|
|
returns table (
|
|
played integer,
|
|
wins integer,
|
|
current_streak integer,
|
|
max_streak integer,
|
|
distribution integer[]
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
round_record record;
|
|
running_streak integer := 0;
|
|
previous_hour timestamptz;
|
|
begin
|
|
if auth.uid() is null then
|
|
raise exception 'Authentication required';
|
|
end if;
|
|
|
|
played := 0;
|
|
wins := 0;
|
|
current_streak := 0;
|
|
max_streak := 0;
|
|
distribution := array[0, 0, 0, 0, 0, 0];
|
|
|
|
for round_record in
|
|
select won, guess_count, hour_start
|
|
from public.wordle_rounds
|
|
where user_id = auth.uid()
|
|
and completed_at is not null
|
|
order by hour_start asc
|
|
loop
|
|
played := played + 1;
|
|
|
|
if round_record.won then
|
|
wins := wins + 1;
|
|
running_streak := case
|
|
when running_streak > 0 and previous_hour is not null and round_record.hour_start = previous_hour + interval '1 hour'
|
|
then running_streak + 1
|
|
else 1
|
|
end;
|
|
max_streak := greatest(max_streak, running_streak);
|
|
|
|
if round_record.guess_count between 1 and 6 then
|
|
distribution[round_record.guess_count] := distribution[round_record.guess_count] + 1;
|
|
end if;
|
|
else
|
|
running_streak := 0;
|
|
end if;
|
|
|
|
previous_hour := round_record.hour_start;
|
|
end loop;
|
|
|
|
previous_hour := null;
|
|
|
|
for round_record in
|
|
select won, hour_start
|
|
from public.wordle_rounds
|
|
where user_id = auth.uid()
|
|
and completed_at is not null
|
|
order by hour_start desc
|
|
loop
|
|
if not round_record.won then
|
|
exit;
|
|
end if;
|
|
|
|
if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
|
|
exit;
|
|
end if;
|
|
|
|
current_streak := current_streak + 1;
|
|
previous_hour := round_record.hour_start;
|
|
end loop;
|
|
|
|
return next;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.is_username_available(candidate_username text)
|
|
returns boolean
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
normalized_username text := nullif(lower(regexp_replace(trim(candidate_username), '[^a-z0-9_]', '', 'g')), '');
|
|
begin
|
|
if normalized_username is null then
|
|
return false;
|
|
end if;
|
|
|
|
return not exists (
|
|
select 1
|
|
from public.profiles
|
|
where lower(username) = normalized_username
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.get_player_current_streak(target_user_id uuid)
|
|
returns integer
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
round_record record;
|
|
streak integer := 0;
|
|
previous_hour timestamptz;
|
|
begin
|
|
for round_record in
|
|
select won, hour_start
|
|
from public.wordle_rounds
|
|
where user_id = target_user_id
|
|
and completed_at is not null
|
|
order by hour_start desc
|
|
loop
|
|
if not round_record.won then
|
|
exit;
|
|
end if;
|
|
|
|
if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
|
|
exit;
|
|
end if;
|
|
|
|
streak := streak + 1;
|
|
previous_hour := round_record.hour_start;
|
|
end loop;
|
|
|
|
return streak;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.get_hourly_leaderboard()
|
|
returns table (
|
|
username text,
|
|
won boolean,
|
|
guess_count integer,
|
|
completed_at timestamptz,
|
|
rank integer
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
begin
|
|
return query
|
|
select
|
|
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
|
rounds.won,
|
|
rounds.guess_count,
|
|
rounds.completed_at,
|
|
row_number() over (
|
|
order by
|
|
rounds.won desc,
|
|
rounds.guess_count asc nulls last,
|
|
rounds.completed_at asc
|
|
)::integer as rank
|
|
from public.wordle_rounds rounds
|
|
left join public.profiles profiles on profiles.id = rounds.user_id
|
|
where rounds.hour_start = date_trunc('hour', now())
|
|
and rounds.completed_at is not null
|
|
order by rank
|
|
limit 25;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.get_leaderboard(board_scope text default 'hour')
|
|
returns table (
|
|
username text,
|
|
won boolean,
|
|
guess_count integer,
|
|
completed_at timestamptz,
|
|
rank integer,
|
|
current_streak integer,
|
|
is_current_user boolean,
|
|
wins integer,
|
|
played integer,
|
|
average_guesses numeric
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
normalized_scope text := lower(coalesce(board_scope, 'hour'));
|
|
begin
|
|
if normalized_scope = 'today' then
|
|
return query
|
|
with aggregate_rows as (
|
|
select
|
|
rounds.user_id,
|
|
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
|
count(*)::integer as played,
|
|
count(*) filter (where rounds.won)::integer as wins,
|
|
round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
|
|
min(rounds.completed_at) filter (where rounds.won) as first_win_at,
|
|
max(rounds.completed_at) as latest_completed_at
|
|
from public.wordle_rounds rounds
|
|
left join public.profiles profiles on profiles.id = rounds.user_id
|
|
where rounds.completed_at is not null
|
|
and rounds.completed_at >= date_trunc('day', now())
|
|
group by rounds.user_id, profiles.username, profiles.display_name
|
|
), ranked as (
|
|
select
|
|
aggregate_rows.*,
|
|
row_number() over (
|
|
order by aggregate_rows.wins desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
|
|
)::integer as row_rank
|
|
from aggregate_rows
|
|
)
|
|
select
|
|
ranked.username,
|
|
ranked.wins > 0,
|
|
null::integer,
|
|
ranked.latest_completed_at,
|
|
ranked.row_rank,
|
|
public.get_player_current_streak(ranked.user_id),
|
|
auth.uid() = ranked.user_id,
|
|
ranked.wins,
|
|
ranked.played,
|
|
ranked.average_guesses
|
|
from ranked
|
|
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
|
order by ranked.row_rank;
|
|
return;
|
|
end if;
|
|
|
|
if normalized_scope = 'all' then
|
|
return query
|
|
with aggregate_rows as (
|
|
select
|
|
rounds.user_id,
|
|
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
|
count(*)::integer as played,
|
|
count(*) filter (where rounds.won)::integer as wins,
|
|
round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
|
|
min(rounds.completed_at) filter (where rounds.won) as first_win_at,
|
|
max(rounds.completed_at) as latest_completed_at
|
|
from public.wordle_rounds rounds
|
|
left join public.profiles profiles on profiles.id = rounds.user_id
|
|
where rounds.completed_at is not null
|
|
group by rounds.user_id, profiles.username, profiles.display_name
|
|
), ranked as (
|
|
select
|
|
aggregate_rows.*,
|
|
row_number() over (
|
|
order by aggregate_rows.wins desc, public.get_player_current_streak(aggregate_rows.user_id) desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
|
|
)::integer as row_rank
|
|
from aggregate_rows
|
|
)
|
|
select
|
|
ranked.username,
|
|
ranked.wins > 0,
|
|
null::integer,
|
|
ranked.latest_completed_at,
|
|
ranked.row_rank,
|
|
public.get_player_current_streak(ranked.user_id),
|
|
auth.uid() = ranked.user_id,
|
|
ranked.wins,
|
|
ranked.played,
|
|
ranked.average_guesses
|
|
from ranked
|
|
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
|
order by ranked.row_rank;
|
|
return;
|
|
end if;
|
|
|
|
return query
|
|
with ranked as (
|
|
select
|
|
rounds.user_id,
|
|
coalesce(profiles.username, profiles.display_name, 'player') as username,
|
|
rounds.won,
|
|
rounds.guess_count,
|
|
rounds.completed_at,
|
|
row_number() over (
|
|
order by rounds.won desc, rounds.guess_count asc nulls last, rounds.completed_at asc
|
|
)::integer as row_rank
|
|
from public.wordle_rounds rounds
|
|
left join public.profiles profiles on profiles.id = rounds.user_id
|
|
where rounds.hour_start = date_trunc('hour', now())
|
|
and rounds.completed_at is not null
|
|
)
|
|
select
|
|
ranked.username,
|
|
ranked.won,
|
|
ranked.guess_count,
|
|
ranked.completed_at,
|
|
ranked.row_rank,
|
|
public.get_player_current_streak(ranked.user_id),
|
|
auth.uid() = ranked.user_id,
|
|
case when ranked.won then 1 else 0 end,
|
|
1,
|
|
ranked.guess_count::numeric
|
|
from ranked
|
|
where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
|
|
order by ranked.row_rank;
|
|
end;
|
|
$$;
|
|
|
|
drop function if exists public.get_player_history(integer);
|
|
|
|
create or replace function public.get_hourly_summary()
|
|
returns table (
|
|
completed_count integer,
|
|
win_rate integer,
|
|
average_guesses numeric
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
begin
|
|
return query
|
|
select
|
|
count(*)::integer,
|
|
coalesce(round((count(*) filter (where won)::numeric / nullif(count(*), 0)) * 100), 0)::integer,
|
|
round(avg(guess_count) filter (where won), 2)
|
|
from public.wordle_rounds
|
|
where hour_start = date_trunc('hour', now())
|
|
and completed_at is not null;
|
|
end;
|
|
$$;
|
|
|
|
drop function if exists public.get_email_for_username(text);
|
|
|
|
create or replace function public.complete_hourly_round(
|
|
round_id uuid,
|
|
did_win boolean,
|
|
guess_total integer
|
|
)
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
begin
|
|
raise exception 'complete_hourly_round is deprecated; use submit_guess';
|
|
end;
|
|
$$;
|
|
|
|
revoke all on public.wordle_words from anon, authenticated;
|
|
revoke all on public.wordle_dictionary from anon, authenticated;
|
|
revoke all on public.profiles from anon, authenticated;
|
|
revoke all on public.wordle_rounds from anon, authenticated;
|
|
revoke all on public.wordle_guesses from anon, authenticated;
|
|
|
|
grant select, update on public.profiles to authenticated;
|
|
grant select on public.wordle_rounds to authenticated;
|
|
grant select on public.wordle_guesses to authenticated;
|
|
grant execute on function public.is_username_available(text) to anon, authenticated;
|
|
grant execute on function public.start_hourly_round() to authenticated;
|
|
grant execute on function public.submit_guess(uuid, text) to authenticated;
|
|
grant execute on function public.get_user_stats() to authenticated;
|
|
grant execute on function public.get_hourly_leaderboard() to anon, authenticated;
|
|
grant execute on function public.get_leaderboard(text) to anon, authenticated;
|
|
grant execute on function public.get_hourly_summary() to anon, authenticated;
|
|
grant execute on function public.complete_hourly_round(uuid, boolean, integer) to authenticated;
|