Important Fixes, New mechanics, and many more
Some checks failed
CI / validate (push) Failing after 9m21s

This commit is contained in:
2026-08-15 17:37:57 +05:00
parent 1774496cf9
commit 438e5af0ad
56 changed files with 5090 additions and 119 deletions

2064
supabase/bootstrap.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,10 @@ create table public.world_entities (
summary text not null,
tags text[] not null default '{}',
secrets jsonb not null default '[]' check (jsonb_typeof(secrets) = 'array'),
search_document tsvector generated always as (to_tsvector('english', coalesce(name, '') || ' ' || coalesce(summary, '') || ' ' || array_to_string(tags, ' '))) stored,
-- Generated expressions may only call IMMUTABLE functions. PostgreSQL marks
-- array_to_string as STABLE, so tags stay queryable through their text[]
-- column while the FTS document indexes the entity's searchable prose.
search_document tsvector generated always as (to_tsvector('english'::regconfig, coalesce(name, '') || ' ' || coalesce(summary, ''))) stored,
created_at timestamptz not null default now()
);
create index world_entities_search_idx on public.world_entities using gin(search_document);
@@ -175,7 +178,7 @@ create table public.memories (
importance integer not null check (importance between 1 and 5),
tags text[] not null default '{}',
entity_ids uuid[] not null default '{}',
search_document tsvector generated always as (to_tsvector('english', coalesce(summary, '') || ' ' || array_to_string(tags, ' '))) stored,
search_document tsvector generated always as (to_tsvector('english'::regconfig, coalesce(summary, ''))) stored,
created_at timestamptz not null default now()
);
create index memories_search_idx on public.memories using gin(search_document);

View File

@@ -0,0 +1,384 @@
-- Stage two multiplayer primitives. All mutating RPCs are service-role only;
-- Nitro authenticates the caller and passes the verified auth.users id.
alter table public.coauthor_sessions
add column if not exists confirmed_world_id uuid references public.worlds(id) on delete set null;
alter table public.worlds add column if not exists hook text;
alter table public.worlds add column if not exists opening_scene text;
create index if not exists coauthor_sessions_owner_updated_idx
on public.coauthor_sessions(owner_id, updated_at desc);
create index if not exists invites_campaign_expiry_idx
on public.invites(campaign_id, expires_at);
create unique index if not exists ai_jobs_world_generation_unique
on public.ai_jobs(entity_id) where job_type = 'generate-world';
create or replace function public.stage_two_append_coauthor_message(
p_session_id uuid,
p_owner_id uuid,
p_content text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_result jsonb;
begin
if p_content is null or char_length(btrim(p_content)) not between 1 and 5000 then
raise exception 'message must be between 1 and 5000 characters';
end if;
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status in ('generating', 'confirmed') then
raise exception 'coauthor session cannot be edited in status %', v_session.status;
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object('role', 'user', 'content', btrim(p_content))),
generated_world = null,
status = 'collecting',
updated_at = now()
where id = p_session_id
returning jsonb_build_object('id', id, 'status', status, 'messages', messages, 'updatedAt', updated_at)
into v_result;
return v_result;
end;
$$;
create or replace function public.stage_two_append_coauthor_assistant_message(
p_session_id uuid,
p_owner_id uuid,
p_content text
) returns void language plpgsql security definer set search_path = '' as $$
begin
if p_content is null or char_length(btrim(p_content)) not between 1 and 5000 then
raise exception 'assistant message must be between 1 and 5000 characters';
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object('role', 'assistant', 'content', btrim(p_content))),
updated_at = now()
where id = p_session_id and owner_id = p_owner_id and status = 'collecting';
if not found then raise exception 'collecting coauthor session not found'; end if;
end;
$$;
create or replace function public.stage_two_open_next_round()
returns trigger language plpgsql security definer set search_path = '' as $$
begin
if old.status is distinct from new.status
and new.status in ('resolved'::public.round_status, 'failed'::public.round_status) then
update public.characters
set controller = 'human'::public.character_controller
where campaign_id = new.campaign_id
and controller = 'delegated'::public.character_controller
and user_id is not null;
if new.status = 'resolved'::public.round_status
and exists (select 1 from public.campaigns where id = new.campaign_id and status = 'active') then
insert into public.rounds(campaign_id, number, status)
values (new.campaign_id, new.number + 1, 'open')
on conflict (campaign_id, number) do nothing;
end if;
end if;
return new;
end;
$$;
drop trigger if exists stage_two_round_resolved_open_next on public.rounds;
create trigger stage_two_round_resolved_open_next
after update of status on public.rounds
for each row execute function public.stage_two_open_next_round();
insert into public.rounds(campaign_id, number, status)
select latest.campaign_id, latest.number + 1, 'open'
from public.rounds latest
join public.campaigns campaign on campaign.id = latest.campaign_id and campaign.status = 'active'
where latest.status = 'resolved'
and not exists (
select 1 from public.rounds newer
where newer.campaign_id = latest.campaign_id and newer.number > latest.number
)
on conflict (campaign_id, number) do nothing;
create or replace function public.stage_two_upsert_story_summary(
p_job_id uuid,
p_campaign_id uuid,
p_through_round integer,
p_summary text
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_summary_id uuid;
begin
if p_through_round < 1 or p_summary is null or char_length(btrim(p_summary)) not between 20 and 6000 then
raise exception 'invalid story summary';
end if;
if not exists (
select 1
from public.ai_jobs job
join public.rounds round on round.id = job.entity_id
where job.id = p_job_id
and job.job_type = 'resolve-round'
and job.status = 'complete'
and round.campaign_id = p_campaign_id
and round.number = p_through_round
and round.status = 'resolved'
) then
raise exception 'completed round job not found';
end if;
insert into public.story_summaries(campaign_id, through_round, summary)
values (p_campaign_id, p_through_round, btrim(p_summary))
on conflict (campaign_id, through_round) do update set summary = excluded.summary
returning id into v_summary_id;
return v_summary_id;
end;
$$;
create or replace function public.stage_two_enqueue_world_generation(
p_session_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_job public.ai_jobs%rowtype;
begin
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status = 'confirmed' then raise exception 'coauthor session is already confirmed'; end if;
if jsonb_array_length(v_session.messages) < 1 then raise exception 'at least one message is required'; end if;
select * into v_job from public.ai_jobs
where job_type = 'generate-world' and entity_id = p_session_id for update;
if found then
if v_job.status in ('queued', 'running') then return v_job.id; end if;
update public.ai_jobs
set status = 'queued', attempts = 0, claimed_by = null, lease_expires_at = null,
available_at = now(), error = null, updated_at = now()
where id = v_job.id;
else
v_job.id := gen_random_uuid();
insert into public.ai_jobs(id, job_type, entity_id, idempotency_key, status)
values (v_job.id, 'generate-world', p_session_id, v_job.id::text, 'queued');
end if;
update public.coauthor_sessions
set status = 'generating', generated_world = null, updated_at = now()
where id = p_session_id;
return v_job.id;
end;
$$;
create or replace function public.stage_two_confirm_world(
p_session_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_world jsonb;
v_world_id uuid;
v_entity jsonb;
begin
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status = 'confirmed' and v_session.confirmed_world_id is not null then
return v_session.confirmed_world_id;
end if;
if v_session.status <> 'ready' or v_session.generated_world is null then
raise exception 'coauthor session is not ready to confirm';
end if;
v_world := v_session.generated_world;
if coalesce(v_world->>'title', '') = '' or coalesce(v_world->>'openingScene', '') = '' then
raise exception 'generated world is incomplete';
end if;
insert into public.worlds(
owner_id, title, genre, tone, premise, content_boundaries,
hidden_threat, hook, opening_scene, status
) values (
p_owner_id, v_world->>'title', v_world->>'genre', v_world->>'tone', v_world->>'premise',
coalesce(v_world->'contentBoundaries', '[]'::jsonb), v_world->>'hiddenThreat',
v_world->>'hook', v_world->>'openingScene', 'confirmed'
) returning id into v_world_id;
v_entity := v_world->'startingLocation';
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
values (v_world_id, 'location', v_entity->>'name', v_entity->>'summary',
array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
coalesce(v_entity->'secrets', '[]'::jsonb));
for v_entity in select * from jsonb_array_elements(v_world->'npcs') loop
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
values (v_world_id, 'npc', v_entity->>'name', v_entity->>'summary',
array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
coalesce(v_entity->'secrets', '[]'::jsonb));
end loop;
for v_entity in select * from jsonb_array_elements(v_world->'factions') loop
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
values (v_world_id, 'faction', v_entity->>'name', v_entity->>'summary',
array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
coalesce(v_entity->'secrets', '[]'::jsonb));
end loop;
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
values (v_world_id, 'quest', 'Opening Hook', v_world->>'hook', array['opening'], '[]'::jsonb);
update public.coauthor_sessions
set status = 'confirmed', confirmed_world_id = v_world_id, updated_at = now()
where id = p_session_id;
return v_world_id;
end;
$$;
create or replace function public.stage_two_create_campaign(
p_world_id uuid,
p_owner_id uuid,
p_title text
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_world public.worlds%rowtype;
v_campaign_id uuid;
begin
if p_title is null or char_length(btrim(p_title)) not between 3 and 100 then
raise exception 'campaign title must be between 3 and 100 characters';
end if;
select * into v_world from public.worlds
where id = p_world_id and owner_id = p_owner_id and status = 'confirmed' for share;
if not found then raise exception 'confirmed world not found'; end if;
insert into public.campaigns(world_id, owner_id, title, current_scene, next_prompt, status)
values (p_world_id, p_owner_id, btrim(p_title), coalesce(v_world.opening_scene, v_world.premise), 'What do you do?', 'active')
returning id into v_campaign_id;
insert into public.campaign_members(campaign_id, user_id, role)
values (v_campaign_id, p_owner_id, 'owner');
insert into public.rounds(campaign_id, number) values (v_campaign_id, 1);
return v_campaign_id;
end;
$$;
create or replace function public.stage_two_join_campaign(
p_token_hash text,
p_user_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_invite public.invites%rowtype;
v_campaign_id uuid;
begin
if p_token_hash is null or char_length(p_token_hash) <> 64 then raise exception 'invalid invite token'; end if;
select * into v_invite from public.invites where token_hash = p_token_hash for update;
if not found or v_invite.expires_at <= now() or v_invite.uses >= v_invite.max_uses then
raise exception 'invite is invalid or expired';
end if;
if not exists (select 1 from public.profiles where id = p_user_id) then raise exception 'profile not found'; end if;
if not exists (select 1 from public.campaigns where id = v_invite.campaign_id and status <> 'archived') then
raise exception 'campaign is not available';
end if;
v_campaign_id := v_invite.campaign_id;
if exists (select 1 from public.campaign_members where campaign_id = v_campaign_id and user_id = p_user_id) then
update public.campaign_members set active = true where campaign_id = v_campaign_id and user_id = p_user_id;
return v_campaign_id;
end if;
insert into public.campaign_members(campaign_id, user_id, role) values (v_campaign_id, p_user_id, 'player');
update public.invites set uses = uses + 1 where id = v_invite.id;
return v_campaign_id;
end;
$$;
create or replace function public.stage_two_submit_intent(
p_round_id uuid,
p_user_id uuid,
p_character_id uuid,
p_action text,
p_ready boolean default false
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_member public.campaign_members%rowtype;
v_intent_id uuid;
v_job_id uuid;
begin
if p_action is null or char_length(btrim(p_action)) not between 1 and 2000 then
raise exception 'action must be between 1 and 2000 characters';
end if;
select * into v_round from public.rounds where id = p_round_id for update;
if not found or v_round.status <> 'open' then raise exception 'round is not open'; end if;
select * into v_member from public.campaign_members
where campaign_id = v_round.campaign_id and user_id = p_user_id and active;
if not found then raise exception 'active campaign membership not found'; end if;
if not exists (
select 1 from public.characters
where id = p_character_id and campaign_id = v_round.campaign_id
and user_id = p_user_id and controller = 'human'
) then raise exception 'controlled character not found'; end if;
insert into public.player_intents(round_id, member_id, character_id, action, ready)
values (p_round_id, v_member.id, p_character_id, btrim(p_action), coalesce(p_ready, false))
on conflict (round_id, member_id) do update
set character_id = excluded.character_id, action = excluded.action,
ready = excluded.ready, updated_at = now()
returning id into v_intent_id;
if coalesce(p_ready, false) and not exists (
select 1 from public.campaign_members member
where member.campaign_id = v_round.campaign_id and member.active
and not exists (
select 1 from public.player_intents intent
where intent.round_id = p_round_id and intent.member_id = member.id and intent.ready
)
) then
v_job_id := public.enqueue_round_resolution(p_round_id, null);
end if;
return jsonb_build_object('intentId', v_intent_id, 'jobId', v_job_id);
end;
$$;
create or replace function public.stage_two_force_round(
p_round_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_job_id uuid;
begin
select * into v_round from public.rounds where id = p_round_id for update;
if not found or v_round.status <> 'open' then raise exception 'round is not open'; end if;
if not exists (
select 1 from public.campaigns
where id = v_round.campaign_id and owner_id = p_owner_id
) then raise exception 'only the campaign owner can force a round'; end if;
update public.characters character
set controller = 'delegated'::public.character_controller
from public.campaign_members member
where character.campaign_id = v_round.campaign_id
and character.user_id = member.user_id
and character.controller = 'human'::public.character_controller
and member.campaign_id = v_round.campaign_id
and member.active
and member.ai_takeover_allowed
and not exists (
select 1 from public.player_intents intent
where intent.round_id = p_round_id
and intent.member_id = member.id
and intent.ready
);
v_job_id := public.enqueue_round_resolution(p_round_id, p_owner_id);
return v_job_id;
end;
$$;
revoke all on function public.stage_two_append_coauthor_message(uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.stage_two_append_coauthor_assistant_message(uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.stage_two_enqueue_world_generation(uuid, uuid) from public, anon, authenticated;
revoke all on function public.stage_two_confirm_world(uuid, uuid) from public, anon, authenticated;
revoke all on function public.stage_two_create_campaign(uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.stage_two_join_campaign(text, uuid) from public, anon, authenticated;
revoke all on function public.stage_two_submit_intent(uuid, uuid, uuid, text, boolean) from public, anon, authenticated;
revoke all on function public.stage_two_force_round(uuid, uuid) from public, anon, authenticated;
revoke all on function public.stage_two_open_next_round() from public, anon, authenticated;
revoke all on function public.stage_two_upsert_story_summary(uuid, uuid, integer, text) from public, anon, authenticated;
grant execute on function public.stage_two_append_coauthor_message(uuid, uuid, text) to service_role;
grant execute on function public.stage_two_append_coauthor_assistant_message(uuid, uuid, text) to service_role;
grant execute on function public.stage_two_enqueue_world_generation(uuid, uuid) to service_role;
grant execute on function public.stage_two_confirm_world(uuid, uuid) to service_role;
grant execute on function public.stage_two_create_campaign(uuid, uuid, text) to service_role;
grant execute on function public.stage_two_join_campaign(text, uuid) to service_role;
grant execute on function public.stage_two_submit_intent(uuid, uuid, uuid, text, boolean) to service_role;
grant execute on function public.stage_two_force_round(uuid, uuid) to service_role;
grant execute on function public.stage_two_upsert_story_summary(uuid, uuid, integer, text) to service_role;
notify pgrst, 'reload schema';

View File

@@ -0,0 +1,100 @@
-- Guest access still uses a real authenticated Supabase user. This keeps the
-- existing ownership checks, RLS policies, and multiplayer membership model.
-- Email accounts remain restricted to the allowlist.
create or replace function public.create_profile_for_allowlisted_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
v_is_anonymous boolean := coalesce(new.is_anonymous, false);
v_display_name text;
begin
if not v_is_anonymous and (
new.email is null or not exists (
select 1
from public.allowlist
where email = lower(btrim(new.email))
)
) then
raise exception using
errcode = '42501',
message = 'This email is not invited to the Dungeons & Ground alpha.';
end if;
v_display_name := case
when v_is_anonymous then 'Guest ' || upper(substr(replace(new.id::text, '-', ''), 1, 6))
else coalesce(nullif(btrim(new.raw_user_meta_data ->> 'display_name'), ''), 'Adventurer')
end;
insert into public.profiles(id, display_name)
values (new.id, v_display_name)
on conflict (id) do nothing;
return new;
end;
$$;
revoke all on function public.create_profile_for_allowlisted_user() from public;
-- An anonymous account can be upgraded later. Recheck the allowlist on the
-- relevant auth.users transition so linking an email cannot bypass the alpha
-- gate. Unrelated token and metadata updates do not fire this trigger.
create or replace function public.enforce_alpha_email_allowlist()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if not coalesce(new.is_anonymous, false) and (
new.email is null or not exists (
select 1
from public.allowlist
where email = lower(btrim(new.email))
)
) then
raise exception using
errcode = '42501',
message = 'This email is not invited to the Dungeons & Ground alpha.';
end if;
return new;
end;
$$;
revoke all on function public.enforce_alpha_email_allowlist() from public;
drop trigger if exists enforce_alpha_email_allowlist on auth.users;
create trigger enforce_alpha_email_allowlist
before update of email, is_anonymous on auth.users
for each row
when (
old.email is distinct from new.email
or old.is_anonymous is distinct from new.is_anonymous
)
execute function public.enforce_alpha_email_allowlist();
-- The schema may be installed after Auth users already exist. Backfill only
-- anonymous users and explicitly allowlisted email accounts; do not silently
-- enable unrelated accounts.
insert into public.profiles(id, display_name)
select
auth_user.id,
case
when coalesce(auth_user.is_anonymous, false)
then 'Guest ' || upper(substr(replace(auth_user.id::text, '-', ''), 1, 6))
else coalesce(nullif(btrim(auth_user.raw_user_meta_data ->> 'display_name'), ''), 'Adventurer')
end
from auth.users auth_user
where coalesce(auth_user.is_anonymous, false)
or exists (
select 1
from public.allowlist
where allowlist.email = lower(btrim(auth_user.email))
)
on conflict (id) do nothing;
notify pgrst, 'reload schema';

View File

@@ -0,0 +1,83 @@
-- Owners can safely retry a round after all worker attempts have failed.
-- The queue has one durable outbox row per round. A retry records the failed
-- state in the audit log, then safely re-queues that same row.
create or replace function public.stage_two_retry_failed_round(
p_round_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_job public.ai_jobs%rowtype;
begin
select * into v_round
from public.rounds
where id = p_round_id
for update;
if not found then raise exception 'round not found'; end if;
if v_round.status <> 'failed'::public.round_status then
raise exception 'round is not failed';
end if;
if not exists (
select 1 from public.campaigns
where id = v_round.campaign_id
and owner_id = p_owner_id
and status = 'active'
) then
raise exception 'only the campaign owner can retry a failed round';
end if;
if exists (
select 1 from public.rounds
where campaign_id = v_round.campaign_id
and id <> p_round_id
and status in ('open'::public.round_status, 'queued'::public.round_status, 'resolving'::public.round_status)
) then
raise exception 'campaign already has an active round';
end if;
select * into v_job
from public.ai_jobs
where job_type = 'resolve-round' and entity_id = p_round_id
for update;
if not found or v_job.status <> 'failed' then
raise exception 'failed round job not found';
end if;
insert into public.audit_entries(
campaign_id, actor_id, action, entity_type, entity_id, before_state, after_state
) values (
v_round.campaign_id,
p_owner_id,
'retry_failed_round',
'round',
p_round_id,
jsonb_build_object('roundError', v_round.error, 'jobError', v_job.error, 'attempts', v_job.attempts),
jsonb_build_object('status', 'queued')
);
update public.ai_jobs
set status = 'queued',
attempts = 0,
claimed_by = null,
lease_expires_at = null,
available_at = now(),
error = null,
updated_at = now()
where id = v_job.id;
update public.rounds
set status = 'queued'::public.round_status,
error = null,
queued_at = now(),
resolved_at = null,
forced_by = p_owner_id
where id = p_round_id;
return v_job.id;
end;
$$;
revoke all on function public.stage_two_retry_failed_round(uuid, uuid) from public, anon, authenticated;
grant execute on function public.stage_two_retry_failed_round(uuid, uuid) to service_role;
notify pgrst, 'reload schema';

View File

@@ -0,0 +1,63 @@
-- Replace the first retry implementation for installations that already ran
-- migration 0005 against the one-job-per-round queue constraint.
create or replace function public.stage_two_retry_failed_round(
p_round_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_job public.ai_jobs%rowtype;
begin
select * into v_round from public.rounds where id = p_round_id for update;
if not found then raise exception 'round not found'; end if;
if v_round.status <> 'failed'::public.round_status then raise exception 'round is not failed'; end if;
if not exists (
select 1 from public.campaigns
where id = v_round.campaign_id and owner_id = p_owner_id and status = 'active'
) then raise exception 'only the campaign owner can retry a failed round'; end if;
if exists (
select 1 from public.rounds
where campaign_id = v_round.campaign_id
and id <> p_round_id
and status in ('open'::public.round_status, 'queued'::public.round_status, 'resolving'::public.round_status)
) then raise exception 'campaign already has an active round'; end if;
select * into v_job
from public.ai_jobs
where job_type = 'resolve-round' and entity_id = p_round_id
for update;
if not found or v_job.status <> 'failed' then raise exception 'failed round job not found'; end if;
insert into public.audit_entries(
campaign_id, actor_id, action, entity_type, entity_id, before_state, after_state
) values (
v_round.campaign_id, p_owner_id, 'retry_failed_round', 'round', p_round_id,
jsonb_build_object('roundError', v_round.error, 'jobError', v_job.error, 'attempts', v_job.attempts),
jsonb_build_object('status', 'queued')
);
update public.ai_jobs
set status = 'queued', attempts = 0, claimed_by = null, lease_expires_at = null,
available_at = now(), error = null, updated_at = now()
where id = v_job.id;
update public.rounds
set status = 'queued'::public.round_status, error = null, queued_at = now(),
resolved_at = null, forced_by = p_owner_id
where id = p_round_id;
return v_job.id;
end;
$$;
create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
select 6;
$$;
revoke all on function public.stage_two_retry_failed_round(uuid, uuid) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_two_retry_failed_round(uuid, uuid) to service_role;
grant execute on function public.dng_schema_version() to service_role;
notify pgrst, 'reload schema';