forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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}