Highly ambitious ATProtocol AppView service and sdks
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}