Highly ambitious ATProtocol AppView service and sdks
138
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 256 lines 8.5 kB view raw
1//! Slice-related queries and statistics. 2//! 3//! This module handles database operations for slice metadata, including 4//! collection statistics, actor counts, lexicon counts, and slice discovery. 5 6use super::client::Database; 7use crate::errors::DatabaseError; 8use crate::models::CollectionStats; 9 10impl Database { 11 /// Gets collection statistics for a slice. 12 /// 13 /// Returns record counts and unique actor counts per collection 14 /// (excluding lexicons marked as excludedFromSync). 15 pub async fn get_slice_collection_stats( 16 &self, 17 slice_uri: &str, 18 ) -> Result<Vec<CollectionStats>, DatabaseError> { 19 let stats = sqlx::query!( 20 r#" 21 WITH slice_collections AS ( 22 SELECT DISTINCT 23 json->>'nsid' as collection_nsid 24 FROM record 25 WHERE collection = 'network.slices.lexicon' 26 AND json->>'slice' = $1 27 AND json->>'nsid' IS NOT NULL 28 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 29 AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 30 ) 31 SELECT 32 r.collection, 33 COUNT(*) as record_count, 34 COUNT(DISTINCT r.did) as unique_actors 35 FROM record r 36 INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid 37 WHERE r.slice_uri = $1 38 GROUP BY r.collection 39 ORDER BY r.collection 40 "#, 41 slice_uri 42 ) 43 .fetch_all(&self.pool) 44 .await?; 45 46 Ok(stats 47 .into_iter() 48 .map(|row| CollectionStats { 49 collection: row.collection, 50 record_count: row.record_count.unwrap_or(0), 51 unique_actors: row.unique_actors.unwrap_or(0), 52 }) 53 .collect()) 54 } 55 56 /// Gets the list of collection NSIDs defined for a slice. 57 /// 58 /// Only includes lexicons with type 'record' that are not excluded from sync. 59 pub async fn get_slice_collections_list( 60 &self, 61 slice_uri: &str, 62 ) -> Result<Vec<String>, DatabaseError> { 63 let rows = sqlx::query!( 64 r#" 65 SELECT DISTINCT json->>'nsid' as collection_nsid 66 FROM record 67 WHERE collection = 'network.slices.lexicon' 68 AND json->>'slice' = $1 69 AND json->>'nsid' IS NOT NULL 70 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 71 AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 72 ORDER BY json->>'nsid' 73 "#, 74 slice_uri 75 ) 76 .fetch_all(&self.pool) 77 .await?; 78 79 Ok(rows 80 .into_iter() 81 .filter_map(|row| row.collection_nsid) 82 .collect()) 83 } 84 85 /// Counts total records across all collections in a slice. 86 /// 87 /// Excludes lexicons marked as excludedFromSync. 88 pub async fn get_slice_total_records(&self, slice_uri: &str) -> Result<i64, DatabaseError> { 89 let count = sqlx::query!( 90 r#" 91 WITH slice_collections AS ( 92 SELECT DISTINCT 93 json->>'nsid' as collection_nsid 94 FROM record 95 WHERE collection = 'network.slices.lexicon' 96 AND json->>'slice' = $1 97 AND json->>'nsid' IS NOT NULL 98 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 99 AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 100 ) 101 SELECT COUNT(*) as count 102 FROM record r 103 INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid 104 WHERE r.slice_uri = $1 105 "#, 106 slice_uri 107 ) 108 .fetch_one(&self.pool) 109 .await?; 110 111 Ok(count.count.unwrap_or(0)) 112 } 113 114 /// Counts total actors tracked in a slice. 115 pub async fn get_slice_total_actors(&self, slice_uri: &str) -> Result<i64, DatabaseError> { 116 let count = sqlx::query!( 117 r#" 118 SELECT COUNT(*) as count 119 FROM actor 120 WHERE slice_uri = $1 121 "#, 122 slice_uri 123 ) 124 .fetch_one(&self.pool) 125 .await?; 126 127 Ok(count.count.unwrap_or(0)) 128 } 129 130 /// Counts lexicon definitions for a slice. 131 /// 132 /// Only includes record-type lexicons. 133 pub async fn get_slice_lexicon_count(&self, slice_uri: &str) -> Result<i64, DatabaseError> { 134 let count = sqlx::query!( 135 r#" 136 SELECT COUNT(*) as count 137 FROM record 138 WHERE collection = 'network.slices.lexicon' 139 AND json->>'slice' = $1 140 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 141 "#, 142 slice_uri 143 ) 144 .fetch_one(&self.pool) 145 .await?; 146 147 Ok(count.count.unwrap_or(0)) 148 } 149 150 /// Gets all slice URIs from network.slices.slice records. 151 /// 152 /// Returns all slices that exist in the system 153 pub async fn get_all_slices(&self) -> Result<Vec<String>, DatabaseError> { 154 let rows: Vec<(String,)> = sqlx::query_as( 155 r#" 156 SELECT DISTINCT uri as slice_uri 157 FROM record 158 WHERE collection = 'network.slices.slice' 159 "#, 160 ) 161 .fetch_all(&self.pool) 162 .await?; 163 164 Ok(rows.into_iter().map(|(uri,)| uri).collect()) 165 } 166 167 /// Gets the domain associated with a slice. 168 /// 169 /// Looks up the network.slices.slice record and extracts the domain field. 170 /// 171 /// # Returns 172 /// Some(domain) if the slice exists and has a domain, None otherwise 173 pub async fn get_slice_domain(&self, slice_uri: &str) -> Result<Option<String>, DatabaseError> { 174 let row = sqlx::query!( 175 r#" 176 SELECT json->>'domain' as domain 177 FROM record 178 WHERE collection = 'network.slices.slice' 179 AND uri = $1 180 "#, 181 slice_uri 182 ) 183 .fetch_optional(&self.pool) 184 .await?; 185 186 Ok(row.and_then(|r| r.domain)) 187 } 188 189 /// Looks up a slice URI by actor handle and rkey. 190 /// 191 /// This method resolves the DID from the actor handle via the actors table, 192 /// then constructs the expected slice URI pattern and finds the matching slice. 193 /// 194 /// # Arguments 195 /// * `actor_handle` - The handle of the slice owner (e.g., "alice.bsky.social") 196 /// * `rkey` - The record key portion of the slice URI 197 /// 198 /// # Returns 199 /// Some(slice_uri) if found, None otherwise 200 pub async fn get_slice_uri_by_handle_and_rkey( 201 &self, 202 actor_handle: &str, 203 rkey: &str, 204 ) -> Result<Option<String>, DatabaseError> { 205 // First, try to get the DID from the actor table 206 let did_row = sqlx::query!( 207 r#" 208 SELECT DISTINCT did 209 FROM actor 210 WHERE handle = $1 211 LIMIT 1 212 "#, 213 actor_handle 214 ) 215 .fetch_optional(&self.pool) 216 .await?; 217 218 if let Some(row) = did_row { 219 // We have a DID, construct the expected URI pattern and look it up 220 let expected_uri_pattern = format!("at://{}/%/{}", row.did, rkey); 221 222 let slice_row = sqlx::query!( 223 r#" 224 SELECT uri 225 FROM record 226 WHERE collection = 'network.slices.slice' 227 AND uri LIKE $1 228 LIMIT 1 229 "#, 230 expected_uri_pattern 231 ) 232 .fetch_optional(&self.pool) 233 .await?; 234 235 Ok(slice_row.map(|r| r.uri)) 236 } else { 237 // No DID found in actors table, try to look up directly by actorHandle in the slice record 238 let slice_row = sqlx::query!( 239 r#" 240 SELECT uri 241 FROM record 242 WHERE collection = 'network.slices.slice' 243 AND json->>'actorHandle' = $1 244 AND uri LIKE '%' || $2 || '%' 245 LIMIT 1 246 "#, 247 actor_handle, 248 rkey 249 ) 250 .fetch_optional(&self.pool) 251 .await?; 252 253 Ok(slice_row.map(|r| r.uri)) 254 } 255 } 256}