···26 SELECT id FROM actors WHERE did = $2
27 )
28 SELECT
29- EXISTS(SELECT 1 FROM follows WHERE actor_id = (SELECT id FROM root_actor) AND subject_actor_id = (SELECT id FROM post_actor)) as following,
30- EXISTS(SELECT 1 FROM follows WHERE actor_id = (SELECT id FROM post_actor) AND subject_actor_id = (SELECT id FROM root_actor)) as followed",
0000000031 &[&root_author, &post_author],
32 )
33 .await?;
···86 SPLIT_PART(SUBSTRING(uri FROM 6), '/', 3) as rkey
87 FROM list_uris
88 ),
89- list_ids AS (
90- SELECT l.id
91 FROM list_parts lp
92 INNER JOIN actors a ON a.did = lp.did
93 INNER JOIN lists l ON l.actor_id = a.id AND l.rkey = lp.rkey
···95 SELECT count(*)
96 FROM list_items li
97 INNER JOIN actors a ON a.did = $2
98- WHERE li.list_id IN (SELECT id FROM list_ids)
99- AND li.subject_actor_id = a.id",
0100 &[&allow_lists, &post_author],
101 )
102 .await?;
···26 SELECT id FROM actors WHERE did = $2
27 )
28 SELECT
29+ EXISTS(
30+ SELECT 1 FROM actors a, unnest(COALESCE(a.following, ARRAY[]::follow_record[])) AS f
31+ WHERE a.id = (SELECT id FROM root_actor)
32+ AND (f).subject_actor_id = (SELECT id FROM post_actor)
33+ ) as following,
34+ EXISTS(
35+ SELECT 1 FROM actors a, unnest(COALESCE(a.following, ARRAY[]::follow_record[])) AS f
36+ WHERE a.id = (SELECT id FROM post_actor)
37+ AND (f).subject_actor_id = (SELECT id FROM root_actor)
38+ ) as followed",
39 &[&root_author, &post_author],
40 )
41 .await?;
···94 SPLIT_PART(SUBSTRING(uri FROM 6), '/', 3) as rkey
95 FROM list_uris
96 ),
97+ list_keys AS (
98+ SELECT a.id as list_owner_actor_id, lp.rkey as list_rkey
99 FROM list_parts lp
100 INNER JOIN actors a ON a.did = lp.did
101 INNER JOIN lists l ON l.actor_id = a.id AND l.rkey = lp.rkey
···103 SELECT count(*)
104 FROM list_items li
105 INNER JOIN actors a ON a.did = $2
106+ INNER JOIN list_keys lk ON li.list_owner_actor_id = lk.list_owner_actor_id
107+ AND li.list_rkey = lk.list_rkey
108+ WHERE li.subject_actor_id = a.id",
109 &[&allow_lists, &post_author],
110 )
111 .await?;
+4-4
consumer/src/db/mod.rs
···134) -> Result<bool> {
135 let row = conn
136 .query_opt(
137- "SELECT 1 FROM thread_mutes
138- WHERE actor_id = $1
139- AND root_post_actor_id = $2
140- AND root_post_rkey = $3",
141 &[&recipient_actor_id, &root_post_actor_id, &root_post_rkey],
142 )
143 .await?;
···134) -> Result<bool> {
135 let row = conn
136 .query_opt(
137+ "SELECT 1 FROM actors, unnest(COALESCE(thread_mutes, ARRAY[]::thread_mute_record[])) AS tm
138+ WHERE id = $1
139+ AND (tm).root_post_actor_id = $2
140+ AND (tm).root_post_rkey = $3",
141 &[&recipient_actor_id, &root_post_actor_id, &root_post_rkey],
142 )
143 .await?;
+36-28
consumer/src/db/operations/graph.rs
···22 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("follows", actor_id, rkey);
23 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
240000000000000025 // BIDIRECTIONAL UPDATE: Update both following[] and followers[] arrays
26- // 1. Add to actor's following array
27- // 2. Add to subject_actor's followers array
28 // Returns number of rows updated (should be 2 if both actors exist)
29 let rows = conn
30 .execute(
31- "WITH follow_record AS (
32- SELECT ROW($2, $3)::follow_record as rec
33 ),
34 update_following AS (
35 UPDATE actors
36 SET following = COALESCE(following, ARRAY[]::follow_record[]) ||
37- (SELECT ARRAY[rec] FROM follow_record)
38 WHERE id = $1
39 AND NOT EXISTS (
40 SELECT 1 FROM unnest(following) f WHERE (f).rkey = $2
···44 update_followers AS (
45 UPDATE actors
46 SET followers = COALESCE(followers, ARRAY[]::follow_record[]) ||
47- ARRAY[ROW($1, $2)::follow_record]
48 WHERE id = $3
49 AND NOT EXISTS (
50- SELECT 1 FROM unnest(followers) f WHERE (f).actor_id = $1 AND (f).rkey = $2
51 )
52 RETURNING 1
53 )
···58 .await?;
5960 // Return 1 if at least one array was updated (follow was created)
61- // Return 0 if neither was updated (duplicate follow)
62 Ok(if rows > 0 { 1 } else { 0 })
63}
64···94 UPDATE actors
95 SET followers = ARRAY(
96 SELECT f FROM unnest(followers) AS f
97- WHERE NOT ((f).actor_id = $2 AND (f).rkey = $1)
98 )
99 WHERE id = (SELECT subject_actor_id FROM follow_to_delete)
100 AND EXISTS (
101 SELECT 1 FROM unnest(followers) f
102- WHERE (f).actor_id = $2 AND (f).rkey = $1
103 )
104 RETURNING 1
105 )
···243244 let (list_did, _collection, list_rkey) = (parts[0], parts[1], parts[2]);
245246- // Resolve list owner DID to actor_id
247- let list_actor_id: i32 = conn.query_one(
248- "INSERT INTO actors (did, handle, created_at)
249- VALUES ($1, NULL, NOW())
250- ON CONFLICT (did) DO UPDATE SET did = EXCLUDED.did
251- RETURNING id",
252- &[&list_did],
253- )
254- .await?
255- .get(0);
256257 // Acquire advisory lock on record to prevent deadlocks
258 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("list_blocks", actor_id, rkey);
259 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
260261 // Append to list_blocks array with natural key deduplication
0262 // Returns number of rows updated (1 if inserted, 0 if duplicate)
263 let rows = conn.execute(
264 "UPDATE actors
···267 WHERE id = $1
268 AND NOT EXISTS (
269 SELECT 1 FROM unnest(list_blocks) lb
270- WHERE (lb).rkey = $2
271 )",
272 &[
273 &actor_id,
274- &rkey,
275- &cid_digest,
276- &list_actor_id,
277- &list_rkey,
278 ],
279 )
280 .await
···320 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("list_items", actor_id, rkey);
321 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
322323- // Resolve list_id by creating stub list if needed
324- let list_id = super::feed::ensure_list_id(conn, &rec.list).await?;
325326 conn.execute(
327 include_str!("../sql/list_item_upsert.sql"),
···331 &cid_digest,
332 // Note: created_at (param $4) removed - derived from TID rkey
333 &rkey,
334- &list_id, // Pass resolved list_id directly
0335 ],
336 )
337 .await
···22 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("follows", actor_id, rkey);
23 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
2425+ // Check if exact same follow already exists (same subject_actor_id and rkey)
26+ let already_exists = conn
27+ .query_opt(
28+ "SELECT 1 FROM actors, unnest(COALESCE(following, ARRAY[]::follow_record[])) AS f
29+ WHERE id = $1 AND (f).subject_actor_id = $2 AND (f).rkey = $3",
30+ &[&actor_id, &subject_actor_id, &rkey],
31+ )
32+ .await?
33+ .is_some();
34+35+ if already_exists {
36+ return Ok(0); // Duplicate, no change needed
37+ }
38+39 // BIDIRECTIONAL UPDATE: Update both following[] and followers[] arrays
40+ // 1. Add to actor's following array - stores (subject_actor_id, rkey)
41+ // 2. Add to subject_actor's followers array - stores (follower_actor_id, rkey) using same follow_record type
42 // Returns number of rows updated (should be 2 if both actors exist)
43 let rows = conn
44 .execute(
45+ "WITH follow_rec AS (
46+ SELECT ROW($3, $2)::follow_record as rec -- (subject_actor_id, rkey)
47 ),
48 update_following AS (
49 UPDATE actors
50 SET following = COALESCE(following, ARRAY[]::follow_record[]) ||
51+ (SELECT ARRAY[rec] FROM follow_rec)
52 WHERE id = $1
53 AND NOT EXISTS (
54 SELECT 1 FROM unnest(following) f WHERE (f).rkey = $2
···58 update_followers AS (
59 UPDATE actors
60 SET followers = COALESCE(followers, ARRAY[]::follow_record[]) ||
61+ ARRAY[ROW($1, $2)::follow_record] -- (follower_actor_id, rkey)
62 WHERE id = $3
63 AND NOT EXISTS (
64+ SELECT 1 FROM unnest(followers) f WHERE (f).subject_actor_id = $1 AND (f).rkey = $2
65 )
66 RETURNING 1
67 )
···72 .await?;
7374 // Return 1 if at least one array was updated (follow was created)
75+ // Return 0 if neither was updated (shouldn't happen since we checked already_exists)
76 Ok(if rows > 0 { 1 } else { 0 })
77}
78···108 UPDATE actors
109 SET followers = ARRAY(
110 SELECT f FROM unnest(followers) AS f
111+ WHERE NOT ((f).subject_actor_id = $2 AND (f).rkey = $1)
112 )
113 WHERE id = (SELECT subject_actor_id FROM follow_to_delete)
114 AND EXISTS (
115 SELECT 1 FROM unnest(followers) f
116+ WHERE (f).subject_actor_id = $2 AND (f).rkey = $1
117 )
118 RETURNING 1
119 )
···257258 let (list_did, _collection, list_rkey) = (parts[0], parts[1], parts[2]);
259260+ // Resolve list owner DID to actor_id (creates stub actor if needed)
261+ let (list_actor_id, _, _) = super::feed::get_actor_id(conn, list_did).await?;
00000000262263 // Acquire advisory lock on record to prevent deadlocks
264 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("list_blocks", actor_id, rkey);
265 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
266267 // Append to list_blocks array with natural key deduplication
268+ // list_block_record has fields: (list_actor_id, list_rkey, rkey, cid)
269 // Returns number of rows updated (1 if inserted, 0 if duplicate)
270 let rows = conn.execute(
271 "UPDATE actors
···274 WHERE id = $1
275 AND NOT EXISTS (
276 SELECT 1 FROM unnest(list_blocks) lb
277+ WHERE (lb).rkey = $4
278 )",
279 &[
280 &actor_id,
281+ &list_actor_id, // $2: list_actor_id
282+ &list_rkey, // $3: list_rkey
283+ &rkey, // $4: rkey
284+ &cid_digest, // $5: cid
285 ],
286 )
287 .await
···327 let (table_id, key_id) = crate::database_writer::locking::actor_record_lock("list_items", actor_id, rkey);
328 crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?;
329330+ // Resolve list natural keys by creating stub list if needed
331+ let (list_owner_actor_id, list_rkey) = super::feed::ensure_list_natural_key(conn, &rec.list).await?;
332333 conn.execute(
334 include_str!("../sql/list_item_upsert.sql"),
···338 &cid_digest,
339 // Note: created_at (param $4) removed - derived from TID rkey
340 &rkey,
341+ &list_owner_actor_id, // Pass resolved list_owner_actor_id (may be NULL)
342+ &list_rkey, // Pass resolved list_rkey (may be NULL)
343 ],
344 )
345 .await
+13-15
consumer/src/db/record_exists/queries.rs
···53 Ok(row.is_some())
54}
5556-/// Check if a follow exists
57pub async fn follow_exists<C: GenericClient>(conn: &C, did: &str, rkey: i64) -> QueryResult<bool> {
58 let row = conn
59 .query_opt(
60- "SELECT 1 FROM follows f
61- INNER JOIN actors a ON f.actor_id = a.id
62- WHERE a.did = $1 AND f.rkey = $2",
63 &[&did, &rkey],
64 )
65 .await?;
66 Ok(row.is_some())
67}
6869-/// Check if a block exists
70pub async fn block_exists<C: GenericClient>(conn: &C, did: &str, rkey: i64) -> QueryResult<bool> {
71 let row = conn
72 .query_opt(
73- "SELECT 1 FROM blocks b
74- INNER JOIN actors a ON b.actor_id = a.id
75- WHERE a.did = $1 AND b.rkey = $2",
76 &[&did, &rkey],
77 )
78 .await?;
···250251/// Check if a labeler exists and should skip fetching
252///
00253/// Returns true if the labeler exists with any status except 'stub'.
254/// - stub: Return false (needs fetching)
255/// - missing: Return true (permanently unfetchable, skip)
···258pub async fn labeler_exists<C: GenericClient>(conn: &C, did: &str) -> QueryResult<bool> {
259 let row = conn
260 .query_opt(
261- "SELECT 1 FROM labelers l
262- INNER JOIN actors a ON l.actor_id = a.id
263- WHERE a.did = $1 AND l.status != 'stub'::labeler_status",
264 &[&did],
265 )
266 .await?;
···279 Ok(row.is_some())
280}
281282-/// Check if a bookmark exists
283pub async fn bookmark_exists<C: GenericClient>(
284 conn: &C,
285 did: &str,
···287) -> QueryResult<bool> {
288 let row = conn
289 .query_opt(
290- "SELECT 1 FROM bookmarks b
291- INNER JOIN actors a ON b.actor_id = a.id
292- WHERE a.did = $1 AND b.rkey = $2",
293 &[&did, &rkey],
294 )
295 .await?;
···53 Ok(row.is_some())
54}
5556+/// Check if a follow exists in the actor's following[] array
57pub async fn follow_exists<C: GenericClient>(conn: &C, did: &str, rkey: i64) -> QueryResult<bool> {
58 let row = conn
59 .query_opt(
60+ "SELECT 1 FROM actors a, unnest(COALESCE(a.following, ARRAY[]::follow_record[])) AS f
61+ WHERE a.did = $1 AND (f).rkey = $2",
062 &[&did, &rkey],
63 )
64 .await?;
65 Ok(row.is_some())
66}
6768+/// Check if a block exists in the actor's blocks[] array
69pub async fn block_exists<C: GenericClient>(conn: &C, did: &str, rkey: i64) -> QueryResult<bool> {
70 let row = conn
71 .query_opt(
72+ "SELECT 1 FROM actors a, unnest(COALESCE(a.blocks, ARRAY[]::block_record[])) AS b
73+ WHERE a.did = $1 AND (b).rkey = $2",
074 &[&did, &rkey],
75 )
76 .await?;
···248249/// Check if a labeler exists and should skip fetching
250///
251+/// DENORMALIZED: Labelers are now stored directly on actors table (labeler_status, labeler_cid, etc)
252+///
253/// Returns true if the labeler exists with any status except 'stub'.
254/// - stub: Return false (needs fetching)
255/// - missing: Return true (permanently unfetchable, skip)
···258pub async fn labeler_exists<C: GenericClient>(conn: &C, did: &str) -> QueryResult<bool> {
259 let row = conn
260 .query_opt(
261+ "SELECT 1 FROM actors
262+ WHERE did = $1 AND labeler_status IS NOT NULL AND labeler_status != 'stub'::labeler_status",
0263 &[&did],
264 )
265 .await?;
···278 Ok(row.is_some())
279}
280281+/// Check if a bookmark exists in the actor's bookmarks[] array
282pub async fn bookmark_exists<C: GenericClient>(
283 conn: &C,
284 did: &str,
···286) -> QueryResult<bool> {
287 let row = conn
288 .query_opt(
289+ "SELECT 1 FROM actors a, unnest(COALESCE(a.bookmarks, ARRAY[]::bookmark_record[])) AS b
290+ WHERE a.did = $1 AND (b).rkey = $2",
0291 &[&did, &rkey],
292 )
293 .await?;
+8-5
consumer/src/db/sql/list_item_upsert.sql
···1--- Insert list_item with self-contained schema (no records table)
2-- This prevents foreign key violations when indexing list items
3--- Parameters: $1=actor_id(INT4), $2=subject_actor_id(i32), $3=cid(bytea), $4=rkey, $5=list_id(INT8)
04-- Note: created_at is derived from TID rkey
5-- Note: actor_id is provided by caller after calling get_actor_id (ensures zero sequence waste)
6-INSERT INTO list_items (actor_id, rkey, cid, list_id, subject_actor_id)
7SELECT
8 $1, -- actor_id (provided by caller)
9 $4, -- rkey (INT8)
10 $3, -- cid (embedded, already bytea)
11- $5, -- list_id (already resolved, may be stub)
012 $2::int4 -- subject_actor_id (already resolved, cast to int4)
13WHERE $1::int4 IS NOT NULL -- Only insert if owner exists
14 AND $2::int4 IS NOT NULL -- Only insert if subject exists
15ON CONFLICT (actor_id, rkey) DO UPDATE SET
16 cid=EXCLUDED.cid,
17- list_id=EXCLUDED.list_id,
018 subject_actor_id=EXCLUDED.subject_actor_id
···1+-- Insert list_item with natural keys (no synthetic list_id)
2-- This prevents foreign key violations when indexing list items
3+-- Parameters: $1=actor_id(INT4), $2=subject_actor_id(i32), $3=cid(bytea), $4=rkey,
4+-- $5=list_owner_actor_id(INT4), $6=list_rkey(TEXT)
5-- Note: created_at is derived from TID rkey
6-- Note: actor_id is provided by caller after calling get_actor_id (ensures zero sequence waste)
7+INSERT INTO list_items (actor_id, rkey, cid, list_owner_actor_id, list_rkey, subject_actor_id)
8SELECT
9 $1, -- actor_id (provided by caller)
10 $4, -- rkey (INT8)
11 $3, -- cid (embedded, already bytea)
12+ $5, -- list_owner_actor_id (already resolved, may be NULL)
13+ $6, -- list_rkey (already resolved, may be NULL)
14 $2::int4 -- subject_actor_id (already resolved, cast to int4)
15WHERE $1::int4 IS NOT NULL -- Only insert if owner exists
16 AND $2::int4 IS NOT NULL -- Only insert if subject exists
17ON CONFLICT (actor_id, rkey) DO UPDATE SET
18 cid=EXCLUDED.cid,
19+ list_owner_actor_id=EXCLUDED.list_owner_actor_id,
20+ list_rkey=EXCLUDED.list_rkey,
21 subject_actor_id=EXCLUDED.subject_actor_id
+6-5
consumer/src/workers/stub_resolution/queries.rs
···7576/// Find stub labelers and return their AT URIs for fetching
77///
0078/// This query finds labelers with status='stub', constructs their AT URIs,
79/// and returns them for enqueuing to the fetch queue.
80/// Limits to 100 records per batch to avoid overwhelming the fetch queue.
81pub async fn find_stub_labelers<C: GenericClient>(conn: &C) -> QueryResult<Vec<String>> {
82 let rows = conn
83 .query(
84- "SELECT 'at://' || a.did || '/app.bsky.labeler.service/self' as uri
85- FROM labelers l
86- INNER JOIN actors a ON l.actor_id = a.id
87- WHERE l.status = 'stub'::labeler_status
88- ORDER BY l.created_at ASC
89 LIMIT 100",
90 &[],
91 )
···7576/// Find stub labelers and return their AT URIs for fetching
77///
78+/// DENORMALIZED: Labelers are now stored directly on actors table (labeler_status, labeler_cid, etc)
79+///
80/// This query finds labelers with status='stub', constructs their AT URIs,
81/// and returns them for enqueuing to the fetch queue.
82/// Limits to 100 records per batch to avoid overwhelming the fetch queue.
83pub async fn find_stub_labelers<C: GenericClient>(conn: &C) -> QueryResult<Vec<String>> {
84 let rows = conn
85 .query(
86+ "SELECT 'at://' || did || '/app.bsky.labeler.service/self' as uri
87+ FROM actors
88+ WHERE labeler_status = 'stub'::labeler_status
89+ ORDER BY labeler_created_at ASC
090 LIMIT 100",
91 &[],
92 )
+28-34
consumer/tests/graph_operations_test.rs
···85 .get(0);
86 assert_eq!(target_count, 1, "Target actor should be created");
8788- // Verify follow relationship exists
89 let follow_count: i64 = tx
90 .query_one(
91- "SELECT COUNT(*) FROM follows f
92- INNER JOIN actors a ON f.actor_id = a.id
93- WHERE a.did = $1 AND f.rkey = tid_to_i64($2)",
94 &[&"did:plc:user123", &"3l7mkz4lmk245"],
95 )
96 .await
···214 "Should return deleted target DID"
215 );
216217- // Verify follow was deleted
218 let follow_count: i64 = tx
219 .query_one(
220- "SELECT COUNT(*) FROM follows f
221- INNER JOIN actors a ON f.actor_id = a.id
222- WHERE a.did = $1 AND f.rkey = tid_to_i64($2)",
223 &[&"did:plc:user789", &"3l7mkz4lmk247"],
224 )
225 .await
···294 .get(0);
295 assert!(blocked_exists, "Blocked actor should exist");
296297- // Verify block relationship
298 let block_count: i64 = tx
299 .query_one(
300- "SELECT COUNT(*) FROM blocks b
301- INNER JOIN actors a ON b.actor_id = a.id
302 WHERE a.did = $1",
303 &[&"did:plc:blocker123"],
304 )
···710 .get(0);
711 assert!(item_exists, "List item should exist");
712713- // Verify list_id is set (not pending)
714- let list_id_is_set: bool = tx
715 .query_one(
716- "SELECT li.list_id IS NOT NULL
717 FROM list_items li
718 INNER JOIN actors a ON li.actor_id = a.id
719 WHERE a.did = $1 AND li.rkey = tid_to_i64($2)",
···722 .await
723 .wrap_err("Failed to query list_items")?
724 .get(0);
725- assert!(list_id_is_set, "List ID should be set (not pending)");
726 Ok(())
727}
728···766 );
767 assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row");
768769- // Verify list_id is set (not NULL) - stub list was created
770- let list_id_is_set: bool = tx
771 .query_one(
772- "SELECT li.list_id IS NOT NULL
773 FROM list_items li
774 INNER JOIN actors a ON li.actor_id = a.id
775 WHERE a.did = $1 AND li.rkey = tid_to_i64($2)",
···778 .await
779 .wrap_err("Failed to query list_items")?
780 .get(0);
781- assert!(list_id_is_set, "List ID should be set (stub list created)");
782783 // Verify the stub list was created with correct status
784 let stub_list_row = tx
···938 );
939 assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row");
940941- // Verify list block exists
942 let block_exists: bool = tx
943 .query_one(
944 "SELECT EXISTS(
945- SELECT 1 FROM list_blocks lb
946- INNER JOIN actors a ON lb.actor_id = a.id
947- WHERE a.did = $1 AND lb.rkey = tid_to_i64($2)
948 )",
949 &[&"did:plc:blocker7", &"3l7mkz4lmk24m"],
950 )
···953 .get(0);
954 assert!(block_exists, "List block should exist");
955956- // Verify list_id is set
957- let list_id_is_set: bool = tx
958 .query_one(
959- "SELECT lb.list_id IS NOT NULL
960- FROM list_blocks lb
961- INNER JOIN actors a ON lb.actor_id = a.id
962- WHERE a.did = $1 AND lb.rkey = tid_to_i64($2)",
963 &[&"did:plc:blocker7", &"3l7mkz4lmk24m"],
964 )
965 .await
966 .wrap_err("Failed to query list_blocks")?
967 .get(0);
968- assert!(list_id_is_set, "List ID should be set");
969 Ok(())
970}
971···1028 );
1029 assert_eq!(result.unwrap(), 1, "Should delete 1 row");
10301031- // Verify list block was deleted
1032 let block_exists: bool = tx
1033 .query_one(
1034 "SELECT EXISTS(
1035- SELECT 1 FROM list_blocks lb
1036- INNER JOIN actors a ON lb.actor_id = a.id
1037- WHERE a.did = $1 AND lb.rkey = tid_to_i64($2)
1038 )",
1039 &[&"did:plc:blocker8", &"3l7mkz4lmk24o"],
1040 )
···85 .get(0);
86 assert_eq!(target_count, 1, "Target actor should be created");
8788+ // Verify follow relationship exists in following[] array
89 let follow_count: i64 = tx
90 .query_one(
91+ "SELECT COUNT(*) FROM actors a, unnest(a.following) AS f
92+ WHERE a.did = $1 AND (f).rkey = tid_to_i64($2)",
093 &[&"did:plc:user123", &"3l7mkz4lmk245"],
94 )
95 .await
···213 "Should return deleted target DID"
214 );
215216+ // Verify follow was deleted from following[] array
217 let follow_count: i64 = tx
218 .query_one(
219+ "SELECT COUNT(*) FROM actors a, unnest(COALESCE(a.following, ARRAY[]::follow_record[])) AS f
220+ WHERE a.did = $1 AND (f).rkey = tid_to_i64($2)",
0221 &[&"did:plc:user789", &"3l7mkz4lmk247"],
222 )
223 .await
···292 .get(0);
293 assert!(blocked_exists, "Blocked actor should exist");
294295+ // Verify block relationship in blocks[] array
296 let block_count: i64 = tx
297 .query_one(
298+ "SELECT COUNT(*) FROM actors a, unnest(a.blocks) AS b
0299 WHERE a.did = $1",
300 &[&"did:plc:blocker123"],
301 )
···707 .get(0);
708 assert!(item_exists, "List item should exist");
709710+ // Verify list natural keys are set (not NULL)
711+ let list_keys_are_set: bool = tx
712 .query_one(
713+ "SELECT li.list_owner_actor_id IS NOT NULL AND li.list_rkey IS NOT NULL
714 FROM list_items li
715 INNER JOIN actors a ON li.actor_id = a.id
716 WHERE a.did = $1 AND li.rkey = tid_to_i64($2)",
···719 .await
720 .wrap_err("Failed to query list_items")?
721 .get(0);
722+ assert!(list_keys_are_set, "List natural keys should be set (not NULL)");
723 Ok(())
724}
725···763 );
764 assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row");
765766+ // Verify list natural keys are set (not NULL) - stub list was created
767+ let list_keys_are_set: bool = tx
768 .query_one(
769+ "SELECT li.list_owner_actor_id IS NOT NULL AND li.list_rkey IS NOT NULL
770 FROM list_items li
771 INNER JOIN actors a ON li.actor_id = a.id
772 WHERE a.did = $1 AND li.rkey = tid_to_i64($2)",
···775 .await
776 .wrap_err("Failed to query list_items")?
777 .get(0);
778+ assert!(list_keys_are_set, "List natural keys should be set (stub list created)");
779780 // Verify the stub list was created with correct status
781 let stub_list_row = tx
···935 );
936 assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row");
937938+ // Verify list block exists in list_blocks[] array
939 let block_exists: bool = tx
940 .query_one(
941 "SELECT EXISTS(
942+ SELECT 1 FROM actors a, unnest(a.list_blocks) AS lb
943+ WHERE a.did = $1 AND (lb).rkey = tid_to_i64($2)
0944 )",
945 &[&"did:plc:blocker7", &"3l7mkz4lmk24m"],
946 )
···949 .get(0);
950 assert!(block_exists, "List block should exist");
951952+ // Verify list natural keys are set
953+ let list_keys_are_set: bool = tx
954 .query_one(
955+ "SELECT (lb).list_actor_id IS NOT NULL AND (lb).list_rkey IS NOT NULL
956+ FROM actors a, unnest(a.list_blocks) AS lb
957+ WHERE a.did = $1 AND (lb).rkey = tid_to_i64($2)",
0958 &[&"did:plc:blocker7", &"3l7mkz4lmk24m"],
959 )
960 .await
961 .wrap_err("Failed to query list_blocks")?
962 .get(0);
963+ assert!(list_keys_are_set, "List natural keys should be set");
964 Ok(())
965}
966···1023 );
1024 assert_eq!(result.unwrap(), 1, "Should delete 1 row");
10251026+ // Verify list block was deleted from list_blocks[] array
1027 let block_exists: bool = tx
1028 .query_one(
1029 "SELECT EXISTS(
1030+ SELECT 1 FROM actors a, unnest(COALESCE(a.list_blocks, ARRAY[]::list_block_record[])) AS lb
1031+ WHERE a.did = $1 AND (lb).rkey = tid_to_i64($2)
01032 )",
1033 &[&"did:plc:blocker8", &"3l7mkz4lmk24o"],
1034 )
+6-3
consumer/tests/notification_test.rs
···101 .wrap_err("Failed to get actor ID")?
102 .get(0);
103104- // Insert a thread mute using natural keys (root_post_actor_id, root_post_rkey)
105- // This is the only place we manually INSERT as there's no function for it
106 tx.execute(
107- "INSERT INTO thread_mutes (actor_id, root_post_actor_id, root_post_rkey, created_at) VALUES ($1, $2, $3, NOW())",
000108 &[&actor_id, &rootpostauthor_id, &rkey_i64],
109 )
110 .await
···101 .wrap_err("Failed to get actor ID")?
102 .get(0);
103104+ // Insert a thread mute into the actors.thread_mutes[] array
105+ // thread_mute_record has fields: (root_post_actor_id, root_post_rkey, created_at)
106 tx.execute(
107+ "UPDATE actors
108+ SET thread_mutes = COALESCE(thread_mutes, ARRAY[]::thread_mute_record[]) ||
109+ ARRAY[ROW($2, $3, NOW())::thread_mute_record]
110+ WHERE id = $1",
111 &[&actor_id, &rootpostauthor_id, &rkey_i64],
112 )
113 .await