From 7b67b51fcd95e60e8645b66b84a4f318621e40f9 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 22 May 2026 12:03:56 -0700 Subject: [PATCH 1/3] feat(notifications): handle_comment_remix_contest_update trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing fourth contest notification trigger. Sibling of handle_event.sql (fan_remix_contest_started) and the two emitted from handle_track.sql (artist_remix_contest_submissions, fan_remix_contest_submission). When the contest host posts a top-level (non-reply) comment on their own remix_contest event, fan out one `remix_contest_update` notification to every active event subscriber except the host. Recipient set mirrors AudiusProject/apps#14159's python create_comment: `subscriptions WHERE entity_type='Event' AND user_id=event_id AND is_current AND NOT is_delete`, excluding the host. The `is_reply` check looks at `comment_threads` — but that row is inserted AFTER the comments row in the same indexer transaction. A plain AFTER INSERT trigger fires before comment_threads is visible and would misclassify every reply as top-level. Solved with DEFERRABLE INITIALLY DEFERRED so the trigger fires at commit time, when both rows are visible. Tests (api/v1_event_comments_notification_test.go): - TestRemixContestUpdate_NotifiesEventSubscribers — host's top-level comment fans out to every non-host event subscriber, with the expected group_id / specifier / data shape - TestRemixContestUpdate_SkipsReplies — host's reply (comment + comment_threads in the same tx) produces zero notifications - TestRemixContestUpdate_SkipsNonHostComments — non-host commenter produces zero notifications Co-Authored-By: Claude Opus 4.7 (1M context) --- api/v1_event_comments_notification_test.go | 304 ++++++++++++++++++ .../handle_comment_remix_contest_update.sql | 110 +++++++ 2 files changed, 414 insertions(+) create mode 100644 api/v1_event_comments_notification_test.go create mode 100644 ddl/functions/handle_comment_remix_contest_update.sql diff --git a/api/v1_event_comments_notification_test.go b/api/v1_event_comments_notification_test.go new file mode 100644 index 00000000..6de0ecaa --- /dev/null +++ b/api/v1_event_comments_notification_test.go @@ -0,0 +1,304 @@ +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{ + "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{ + "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": "blk-parent", "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{ + "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 $$; From 55a1e17bc9838233b7dcda5a579cf2af3533d3f2 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 22 May 2026 12:17:14 -0700 Subject: [PATCH 2/3] test(notifications): seed blocks rows for comment FK in remix-contest tests comments.blocknumber has an FK to blocks.number. The three new tests inserted comments without first seeding the matching blocks rows, so CI failed with comments_blocknumber_fkey (SQLSTATE 23503). Add the missing blocks fixtures (and align the parent-comment's blockhash with the seeded blocks row in the SkipsReplies test). Co-Authored-By: Claude Opus 4.7 (1M context) --- api/v1_event_comments_notification_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api/v1_event_comments_notification_test.go b/api/v1_event_comments_notification_test.go index 6de0ecaa..fef83101 100644 --- a/api/v1_event_comments_notification_test.go +++ b/api/v1_event_comments_notification_test.go @@ -29,6 +29,11 @@ func TestRemixContestUpdate_NotifiesEventSubscribers(t *testing.T) { 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"}, @@ -175,6 +180,13 @@ func TestRemixContestUpdate_SkipsReplies(t *testing.T) { 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"}, @@ -199,7 +211,7 @@ func TestRemixContestUpdate_SkipsReplies(t *testing.T) { "entity_id": eventId, "entity_type": "Event", "text": "opener", "is_delete": false, "is_visible": true, "created_at": now, "updated_at": now, - "txhash": "tx-parent", "blockhash": "blk-parent", "blocknumber": 99}, + "txhash": "tx-parent", "blockhash": "rcu-blk-99", "blocknumber": 99}, }, } database.Seed(app.pool.Replicas[0], fixtures) @@ -257,6 +269,10 @@ func TestRemixContestUpdate_SkipsNonHostComments(t *testing.T) { 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"}, From 4da78ab3c7bdc9008d49124dbad49321cbc943c4 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 22 May 2026 12:46:26 -0700 Subject: [PATCH 3/3] chore(schema): regenerate dump with handle_comment_remix_contest_update The test DB is bootstrapped from sql/01_schema.sql (mounted into postgres' /docker-entrypoint-initdb.d). New ddl/functions files don't take effect in tests until the schema dump is regenerated. Adding the function + constraint trigger here so the new tests find them on a fresh template. Co-Authored-By: Claude Opus 4.7 (1M context) --- sql/01_schema.sql | 204 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 12 deletions(-) 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: - --