diff --git a/api/v1_event_comments_notification_test.go b/api/v1_event_comments_notification_test.go new file mode 100644 index 00000000..fef83101 --- /dev/null +++ b/api/v1_event_comments_notification_test.go @@ -0,0 +1,320 @@ +package api + +import ( + "context" + "testing" + "time" + + "api.audius.co/database" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestRemixContestUpdate_NotifiesEventSubscribers exercises the +// handle_comment_remix_contest_update trigger. When the contest host posts +// a top-level (non-reply) comment on their own remix-contest event, every +// active event subscriber (excluding the host) should receive one +// `remix_contest_update` notification row. +func TestRemixContestUpdate_NotifiesEventSubscribers(t *testing.T) { + app := emptyTestApp(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + hostId := 7101 + subA := 7102 + subB := 7103 + parentTrackId := 7201 + eventId := 7301 + commentId := 7401 + + now := time.Now().UTC() + fixtures := database.FixtureMap{ + // comments has an FK on blocknumber → blocks.number; the comment we + // insert later uses blocknumber=100, so seed it. + "blocks": []map[string]any{ + {"blockhash": "rcu-blk-100", "parenthash": nil, "number": 100}, + }, + "users": []map[string]any{ + {"user_id": hostId, "handle": "rc_host"}, + {"user_id": subA, "handle": "rc_subA"}, + {"user_id": subB, "handle": "rc_subB"}, + }, + "tracks": []map[string]any{ + { + "track_id": parentTrackId, + "owner_id": hostId, + "title": "Parent Track", + "created_at": now, + "updated_at": now, + }, + }, + "events": []map[string]any{ + { + "event_id": eventId, + "event_type": "remix_contest", + "entity_id": parentTrackId, + "user_id": hostId, + "created_at": now, + "end_date": now.Add(7 * 24 * time.Hour), + }, + }, + "subscriptions": []map[string]any{ + // Both subA and subB subscribe to the event. Host also "subscribes" + // to their own event (which should still be excluded by the trigger). + { + "subscriber_id": subA, + "user_id": eventId, + "entity_type": "Event", + "entity_id": eventId, + "is_current": true, + "is_delete": false, + "created_at": now, + "txhash": "seed-subA", + }, + { + "subscriber_id": subB, + "user_id": eventId, + "entity_type": "Event", + "entity_id": eventId, + "is_current": true, + "is_delete": false, + "created_at": now, + "txhash": "seed-subB", + }, + { + "subscriber_id": hostId, + "user_id": eventId, + "entity_type": "Event", + "entity_id": eventId, + "is_current": true, + "is_delete": false, + "created_at": now, + "txhash": "seed-subhost", + }, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + // Host posts a TOP-LEVEL comment on their own event. Single auto-commit + // statement → deferred trigger fires at the commit → comment_threads is + // empty for this comment_id → treated as top-level → fans out. + _, err := app.writePool.Exec(ctx, ` + INSERT INTO comments ( + comment_id, user_id, entity_id, entity_type, + text, is_delete, is_visible, is_edited, + created_at, updated_at, + txhash, blockhash, blocknumber + ) VALUES ( + $1, $2, $3, 'Event', + 'Round 1 is live!', false, true, false, + $4, $4, + 'tx-rcu-1', 'blk-rcu-1', 100 + ) + `, commentId, hostId, eventId, now) + require.NoError(t, err) + + // Each non-host subscriber should now have exactly one + // remix_contest_update notification. + type notifRow struct { + Specifier string + GroupId string + UserIds []int32 + EventId int + EntityId int + EntityUserId int + CommentId int + } + rows, err := app.writePool.Query(ctx, ` + SELECT specifier, group_id, user_ids, + (data->>'event_id')::int, + (data->>'entity_id')::int, + (data->>'entity_user_id')::int, + (data->>'comment_id')::int + FROM notification + WHERE type = 'remix_contest_update' + AND group_id = $1 + ORDER BY specifier ASC + `, "remix_contest_update:7401:event:7301") + require.NoError(t, err) + defer rows.Close() + + var got []notifRow + for rows.Next() { + var r notifRow + require.NoError(t, rows.Scan(&r.Specifier, &r.GroupId, &r.UserIds, + &r.EventId, &r.EntityId, &r.EntityUserId, &r.CommentId)) + got = append(got, r) + } + require.NoError(t, rows.Err()) + + require.Len(t, got, 2, "expected exactly one notification per non-host subscriber") + + recipients := map[int32]bool{} + for _, r := range got { + assert.Equal(t, eventId, r.EventId) + assert.Equal(t, parentTrackId, r.EntityId, "entity_id = the contest's parent track") + assert.Equal(t, hostId, r.EntityUserId, "entity_user_id = the event host") + assert.Equal(t, commentId, r.CommentId) + require.Len(t, r.UserIds, 1) + recipients[r.UserIds[0]] = true + } + assert.True(t, recipients[int32(subA)], "subA must receive the notification") + assert.True(t, recipients[int32(subB)], "subB must receive the notification") + assert.False(t, recipients[int32(hostId)], "host must NOT receive a notification for their own post") +} + +// TestRemixContestUpdate_SkipsReplies verifies that a HOST reply (a +// comment with a comment_threads row inserted in the same transaction) +// does NOT trigger remix_contest_update. Only top-level posts do. +func TestRemixContestUpdate_SkipsReplies(t *testing.T) { + app := emptyTestApp(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + hostId := 7501 + subId := 7502 + parentTrackId := 7601 + eventId := 7701 + parentCommentId := 7801 + replyCommentId := 7802 + + now := time.Now().UTC() + fixtures := database.FixtureMap{ + // comments has an FK on blocknumber → blocks.number; both the seeded + // parent comment (blocknumber=99) and the reply we insert later + // (blocknumber=100) need their blocks rows. + "blocks": []map[string]any{ + {"blockhash": "rcu-blk-99", "parenthash": nil, "number": 99}, + {"blockhash": "rcu-blk-100", "parenthash": "rcu-blk-99", "number": 100}, + }, + "users": []map[string]any{ + {"user_id": hostId, "handle": "rcr_host"}, + {"user_id": subId, "handle": "rcr_sub"}, + }, + "tracks": []map[string]any{ + {"track_id": parentTrackId, "owner_id": hostId, "title": "Parent", + "created_at": now, "updated_at": now}, + }, + "events": []map[string]any{ + {"event_id": eventId, "event_type": "remix_contest", + "entity_id": parentTrackId, "user_id": hostId, + "created_at": now, "end_date": now.Add(7 * 24 * time.Hour)}, + }, + "subscriptions": []map[string]any{ + {"subscriber_id": subId, "user_id": eventId, "entity_type": "Event", + "entity_id": eventId, "is_current": true, "is_delete": false, + "created_at": now, "txhash": "seed-sub-r"}, + }, + // Seed an existing top-level host comment to be the reply's parent. + "comments": []map[string]any{ + {"comment_id": parentCommentId, "user_id": hostId, + "entity_id": eventId, "entity_type": "Event", + "text": "opener", "is_delete": false, "is_visible": true, + "created_at": now, "updated_at": now, + "txhash": "tx-parent", "blockhash": "rcu-blk-99", "blocknumber": 99}, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + // We need the comment INSERT + comment_threads INSERT in the same tx so + // the deferred trigger sees the thread row at commit time. + tx, err := app.writePool.Begin(ctx) + require.NoError(t, err) + _, err = tx.Exec(ctx, ` + INSERT INTO comments ( + comment_id, user_id, entity_id, entity_type, + text, is_delete, is_visible, is_edited, + created_at, updated_at, + txhash, blockhash, blocknumber + ) VALUES ( + $1, $2, $3, 'Event', + 'my reply', false, true, false, + $4, $4, + 'tx-reply', 'blk-reply', 100 + ) + `, replyCommentId, hostId, eventId, now) + require.NoError(t, err) + _, err = tx.Exec(ctx, ` + INSERT INTO comment_threads (parent_comment_id, comment_id) + VALUES ($1, $2) + `, parentCommentId, replyCommentId) + require.NoError(t, err) + require.NoError(t, tx.Commit(ctx)) + + // No remix_contest_update notification should exist for the reply. + var n int + err = app.writePool.QueryRow(ctx, ` + SELECT count(*) FROM notification + WHERE type = 'remix_contest_update' + AND group_id = $1 + `, "remix_contest_update:7802:event:7701").Scan(&n) + require.NoError(t, err) + assert.Equal(t, 0, n, "host replies must NOT trigger remix_contest_update") +} + +// TestRemixContestUpdate_SkipsNonHostComments verifies that a NON-host +// commenter on the event does not trigger the notification, even if +// they're commenting top-level. +func TestRemixContestUpdate_SkipsNonHostComments(t *testing.T) { + app := emptyTestApp(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + hostId := 7901 + commenterId := 7902 + subId := 7903 + parentTrackId := 7801 + eventId := 7811 + commentId := 7821 + + now := time.Now().UTC() + fixtures := database.FixtureMap{ + // comments has an FK on blocknumber → blocks.number. + "blocks": []map[string]any{ + {"blockhash": "rcu-blk-100", "parenthash": nil, "number": 100}, + }, + "users": []map[string]any{ + {"user_id": hostId, "handle": "rcn_host"}, + {"user_id": commenterId, "handle": "rcn_commenter"}, + {"user_id": subId, "handle": "rcn_sub"}, + }, + "tracks": []map[string]any{ + {"track_id": parentTrackId, "owner_id": hostId, "title": "Parent", + "created_at": now, "updated_at": now}, + }, + "events": []map[string]any{ + {"event_id": eventId, "event_type": "remix_contest", + "entity_id": parentTrackId, "user_id": hostId, + "created_at": now, "end_date": now.Add(7 * 24 * time.Hour)}, + }, + "subscriptions": []map[string]any{ + {"subscriber_id": subId, "user_id": eventId, "entity_type": "Event", + "entity_id": eventId, "is_current": true, "is_delete": false, + "created_at": now, "txhash": "seed-sub-nh"}, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + // Non-host posts a top-level comment on the event. + _, err := app.writePool.Exec(ctx, ` + INSERT INTO comments ( + comment_id, user_id, entity_id, entity_type, + text, is_delete, is_visible, is_edited, + created_at, updated_at, + txhash, blockhash, blocknumber + ) VALUES ( + $1, $2, $3, 'Event', + 'I hope I win!', false, true, false, + $4, $4, + 'tx-nh', 'blk-nh', 100 + ) + `, commentId, commenterId, eventId, now) + require.NoError(t, err) + + var n int + err = app.writePool.QueryRow(ctx, ` + SELECT count(*) FROM notification WHERE type = 'remix_contest_update' + `).Scan(&n) + require.NoError(t, err) + assert.Equal(t, 0, n, "non-host comments must NOT trigger remix_contest_update") +} diff --git a/ddl/functions/handle_comment_remix_contest_update.sql b/ddl/functions/handle_comment_remix_contest_update.sql new file mode 100644 index 00000000..46ca6a3f --- /dev/null +++ b/ddl/functions/handle_comment_remix_contest_update.sql @@ -0,0 +1,110 @@ +-- handle_comment_remix_contest_update +-- +-- Emits a `remix_contest_update` notification to every event subscriber +-- (except the host) when the contest host posts a TOP-LEVEL comment on +-- their own remix-contest event. +-- +-- Sibling of handle_event.sql / handle_track.sql which already emit the +-- other three contest notifications: +-- - handle_event.sql: fan_remix_contest_started +-- - handle_track.sql: artist_remix_contest_submissions +-- fan_remix_contest_submission +-- +-- Why DEFERRABLE INITIALLY DEFERRED: +-- "Top-level" is determined by the absence of a comment_threads row for +-- this comment_id. The indexer inserts that row AFTER the comments row, +-- in the same transaction. A plain AFTER INSERT trigger on comments +-- would fire before comment_threads is populated and incorrectly treat +-- every reply as top-level. A deferred constraint trigger fires at +-- commit time, by which point both rows are visible. +create or replace function handle_comment_remix_contest_update() returns trigger as $$ +declare + event_host_id int; + contest_track_id int; + recipient_id int; + group_id_str text; + data_jsonb jsonb; +begin + -- Cheap pre-filters first. + if new.entity_type <> 'Event' or new.is_delete or not new.is_visible then + return null; + end if; + + -- Bail if this comment is a reply (a comment_threads row was inserted + -- alongside or before commit). Replies do not produce + -- remix_contest_update — only the host's top-level posts do. + if exists ( + select 1 from comment_threads where comment_id = new.comment_id + ) then + return null; + end if; + + -- The event must exist, must be a remix_contest, and the commenter + -- must be the host. entity_id on events is the parent track id. + select e.user_id, e.entity_id + into event_host_id, contest_track_id + from events e + where e.event_id = new.entity_id + and e.event_type = 'remix_contest' + and e.is_deleted = false + limit 1; + + if event_host_id is null or event_host_id <> new.user_id then + return null; + end if; + + group_id_str := 'remix_contest_update:' || new.comment_id || ':event:' || new.entity_id; + data_jsonb := jsonb_build_object( + 'event_id', new.entity_id, + 'entity_id', contest_track_id, + 'entity_user_id', event_host_id, + 'comment_id', new.comment_id + ); + + -- Fan out to subscribers, excluding the host (they have their own + -- view of the post). + for recipient_id in + select s.subscriber_id + from subscriptions s + where s.entity_type = 'Event' + and s.user_id = new.entity_id + and s.is_current = true + and s.is_delete = false + and s.subscriber_id <> event_host_id + loop + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY[recipient_id], + new.created_at, + 'remix_contest_update', + recipient_id::text, + group_id_str, + data_jsonb + ) + on conflict do nothing; + end loop; + + return null; + +exception + when others then + raise warning 'An error occurred in %: %', tg_name, sqlerrm; + return null; +end; +$$ language plpgsql; + + +do $$ begin + -- Deferred so it fires at commit time, after the sibling + -- comment_threads insert (if any) is also visible. Without that, we'd + -- misclassify every reply as a top-level post. + create constraint trigger on_comment_remix_contest_update + after insert on comments + deferrable initially deferred + for each row execute procedure handle_comment_remix_contest_update(); +exception + when others then null; +end $$; diff --git a/sql/01_schema.sql b/sql/01_schema.sql index 0a98f82b..bf48e97b 100644 --- a/sql/01_schema.sql +++ b/sql/01_schema.sql @@ -3,8 +3,8 @@ -- --- Dumped from database version 17.10 (Debian 17.10-1.pgdg13+1) --- Dumped by pg_dump version 17.10 (Debian 17.10-1.pgdg13+1) +-- Dumped from database version 17.9 (Debian 17.9-1.pgdg13+1) +-- Dumped by pg_dump version 17.9 (Debian 17.9-1.pgdg13+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -2336,6 +2336,92 @@ end; $$; +-- +-- Name: handle_comment_remix_contest_update(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_comment_remix_contest_update() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + event_host_id int; + contest_track_id int; + recipient_id int; + group_id_str text; + data_jsonb jsonb; +begin + -- Cheap pre-filters first. + if new.entity_type <> 'Event' or new.is_delete or not new.is_visible then + return null; + end if; + + -- Bail if this comment is a reply (a comment_threads row was inserted + -- alongside or before commit). Replies do not produce + -- remix_contest_update -- only the host's top-level posts do. + if exists ( + select 1 from comment_threads where comment_id = new.comment_id + ) then + return null; + end if; + + -- The event must exist, must be a remix_contest, and the commenter + -- must be the host. entity_id on events is the parent track id. + select e.user_id, e.entity_id + into event_host_id, contest_track_id + from events e + where e.event_id = new.entity_id + and e.event_type = 'remix_contest' + and e.is_deleted = false + limit 1; + + if event_host_id is null or event_host_id <> new.user_id then + return null; + end if; + + group_id_str := 'remix_contest_update:' || new.comment_id || ':event:' || new.entity_id; + data_jsonb := jsonb_build_object( + 'event_id', new.entity_id, + 'entity_id', contest_track_id, + 'entity_user_id', event_host_id, + 'comment_id', new.comment_id + ); + + -- Fan out to subscribers, excluding the host (they have their own + -- view of the post). + for recipient_id in + select s.subscriber_id + from subscriptions s + where s.entity_type = 'Event' + and s.user_id = new.entity_id + and s.is_current = true + and s.is_delete = false + and s.subscriber_id <> event_host_id + loop + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY[recipient_id], + new.created_at, + 'remix_contest_update', + recipient_id::text, + group_id_str, + data_jsonb + ) + on conflict do nothing; + end loop; + + return null; + +exception + when others then + raise warning 'An error occurred in %: %', tg_name, sqlerrm; + return null; +end; +$$; + + -- -- Name: handle_comms_rpc_log(); Type: FUNCTION; Schema: public; Owner: - -- @@ -8757,6 +8843,25 @@ COMMENT ON COLUMN public.sol_reward_manager_inits.manager IS 'Public key of the COMMENT ON COLUMN public.sol_reward_manager_inits.authority IS 'Public key of the authority account, which holds the token accounts that reward manager can disburse from'; +-- +-- Name: sol_slot_checkpoint; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sol_slot_checkpoint ( + id integer DEFAULT 1 NOT NULL, + slot bigint NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: TABLE sol_slot_checkpoint; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.sol_slot_checkpoint IS 'Stores the most recent slot that the indexer has received.'; + + -- -- Name: sol_slot_checkpoints; Type: TABLE; Schema: public; Owner: - -- @@ -8787,6 +8892,30 @@ COMMENT ON TABLE public.sol_slot_checkpoints IS 'Stores checkpoints for Solana s COMMENT ON COLUMN public.sol_slot_checkpoints.name IS 'The name of the indexer this checkpoint is for (e.g., token_indexer, damm_v2_indexer).'; +-- +-- Name: sol_swaps; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sol_swaps ( + signature character varying NOT NULL, + instruction_index integer NOT NULL, + slot bigint NOT NULL, + from_mint character varying NOT NULL, + from_account character varying NOT NULL, + from_amount bigint NOT NULL, + to_mint character varying NOT NULL, + to_account character varying NOT NULL, + to_amount bigint NOT NULL +); + + +-- +-- Name: TABLE sol_swaps; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.sol_swaps IS 'Stores eg. Jupiter swaps for tracked mints.'; + + -- -- Name: sol_token_account_balance_changes; Type: TABLE; Schema: public; Owner: - -- @@ -9716,7 +9845,7 @@ CREATE VIEW public.v_usdc_purchases AS WHERE (((pay.signature)::text = (sp.signature)::text) AND (pay.instruction_index = sp.instruction_index))) AS splits FROM ((public.sol_purchases sp LEFT JOIN public.tracks t ON ((((sp.content_type)::text = 'track'::text) AND (t.track_id = sp.content_id) AND (t.is_current = true)))) - LEFT JOIN public.playlists p ON ((((sp.content_type)::text = ANY ((ARRAY['album'::character varying, 'playlist'::character varying])::text[])) AND (p.playlist_id = sp.content_id) AND (p.is_current = true)))) + LEFT JOIN public.playlists p ON ((((sp.content_type)::text = ANY (ARRAY[('album'::character varying)::text, ('playlist'::character varying)::text])) AND (p.playlist_id = sp.content_id) AND (p.is_current = true)))) WHERE (sp.is_valid IS TRUE); @@ -10906,6 +11035,14 @@ ALTER TABLE ONLY public.sol_reward_manager_inits ADD CONSTRAINT sol_reward_manager_inits_pkey PRIMARY KEY (signature, instruction_index); +-- +-- Name: sol_slot_checkpoint sol_slot_checkpoint_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sol_slot_checkpoint + ADD CONSTRAINT sol_slot_checkpoint_pkey PRIMARY KEY (id); + + -- -- Name: sol_slot_checkpoints sol_slot_checkpoints_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -10914,6 +11051,14 @@ ALTER TABLE ONLY public.sol_slot_checkpoints ADD CONSTRAINT sol_slot_checkpoints_pkey PRIMARY KEY (id); +-- +-- Name: sol_swaps sol_swaps_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sol_swaps + ADD CONSTRAINT sol_swaps_pkey PRIMARY KEY (signature, instruction_index); + + -- -- Name: sol_token_account_balance_changes sol_token_account_balance_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -12450,39 +12595,67 @@ CREATE INDEX sol_slot_checkpoints_from_slot_idx ON public.sol_slot_checkpoints U CREATE INDEX sol_slot_checkpoints_to_slot_idx ON public.sol_slot_checkpoints USING btree (subscription_hash, to_slot); +-- +-- Name: sol_swaps_from_account_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX sol_swaps_from_account_idx ON public.sol_swaps USING btree (from_account); + + +-- +-- Name: sol_swaps_from_mint_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX sol_swaps_from_mint_idx ON public.sol_swaps USING btree (from_mint); + + +-- +-- Name: sol_swaps_to_account_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX sol_swaps_to_account_idx ON public.sol_swaps USING btree (to_account); + + +-- +-- Name: sol_swaps_to_mint_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX sol_swaps_to_mint_idx ON public.sol_swaps USING btree (to_mint); + + -- -- Name: sol_token_account_balance_changes_account_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX sol_token_account_balance_changes_account_idx ON public.sol_token_account_balance_changes USING btree (account, slot); +CREATE INDEX sol_token_account_balance_changes_account_idx ON public.sol_token_account_balance_changes USING btree (account, slot DESC); -- --- Name: INDEX sol_token_account_balance_changes_account_idx; Type: COMMENT; Schema: public; Owner: - +-- Name: sol_token_account_balance_changes_mint_account_slot_idx; Type: INDEX; Schema: public; Owner: - -- -COMMENT ON INDEX public.sol_token_account_balance_changes_account_idx IS 'Used for getting recent transactions by account.'; +CREATE INDEX sol_token_account_balance_changes_mint_account_slot_idx ON public.sol_token_account_balance_changes USING btree (mint, account, slot DESC); -- --- Name: sol_token_account_balance_changes_mint_block_timestamp; Type: INDEX; Schema: public; Owner: - +-- Name: INDEX sol_token_account_balance_changes_mint_account_slot_idx; Type: COMMENT; Schema: public; Owner: - -- -CREATE INDEX sol_token_account_balance_changes_mint_block_timestamp ON public.sol_token_account_balance_changes USING btree (mint, block_timestamp DESC); +COMMENT ON INDEX public.sol_token_account_balance_changes_mint_account_slot_idx IS 'Used for getting top current balances for a mint.'; -- --- Name: sol_token_account_balance_changes_mint_idx; Type: INDEX; Schema: public; Owner: - +-- Name: sol_token_account_balance_changes_mint_block_timestamp; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX sol_token_account_balance_changes_mint_idx ON public.sol_token_account_balance_changes USING btree (mint, slot); +CREATE INDEX sol_token_account_balance_changes_mint_block_timestamp ON public.sol_token_account_balance_changes USING btree (mint, block_timestamp DESC); -- --- Name: INDEX sol_token_account_balance_changes_mint_idx; Type: COMMENT; Schema: public; Owner: - +-- Name: sol_token_account_balance_changes_mint_idx; Type: INDEX; Schema: public; Owner: - -- -COMMENT ON INDEX public.sol_token_account_balance_changes_mint_idx IS 'Used for getting recent transactions by mint.'; +CREATE INDEX sol_token_account_balance_changes_mint_idx ON public.sol_token_account_balance_changes USING btree (mint, slot DESC); -- @@ -12807,6 +12980,13 @@ CREATE TRIGGER on_chat_message_reaction_changed AFTER INSERT OR DELETE OR UPDATE CREATE TRIGGER on_comment AFTER INSERT ON public.comments FOR EACH ROW EXECUTE FUNCTION public.handle_comment(); +-- +-- Name: comments on_comment_remix_contest_update; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE CONSTRAINT TRIGGER on_comment_remix_contest_update AFTER INSERT ON public.comments DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION public.handle_comment_remix_contest_update(); + + -- -- Name: sol_meteora_dbc_pools on_dbc_pool_change; Type: TRIGGER; Schema: public; Owner: - --