@@ -151,3 +151,152 @@ func GetForkById(forkId uint64) *dbtypes.Fork {
151151
152152 return & fork
153153}
154+
155+ func GetForkVisualizationData (startSlot uint64 , endSlot uint64 ) ([]* dbtypes.Fork , error ) {
156+ forks := []* dbtypes.Fork {}
157+
158+ // Get forks that overlap with our time window and their direct parents/children
159+ err := ReaderDb .Select (& forks , `
160+ SELECT DISTINCT fork_id, base_slot, base_root, leaf_slot, leaf_root, parent_fork
161+ FROM (
162+ -- Forks that overlap with our time window
163+ SELECT fork_id, base_slot, base_root, leaf_slot, leaf_root, parent_fork
164+ FROM forks
165+ WHERE base_slot < $2 AND leaf_slot >= $1
166+
167+ UNION
168+
169+ -- Direct parents of forks in our window
170+ SELECT p.fork_id, p.base_slot, p.base_root, p.leaf_slot, p.leaf_root, p.parent_fork
171+ FROM forks p
172+ INNER JOIN forks f ON p.fork_id = f.parent_fork
173+ WHERE f.base_slot < $2 AND f.leaf_slot >= $1
174+
175+ UNION
176+
177+ -- Direct children of forks in our window
178+ SELECT c.fork_id, c.base_slot, c.base_root, c.leaf_slot, c.leaf_root, c.parent_fork
179+ FROM forks c
180+ INNER JOIN forks f ON c.parent_fork = f.fork_id
181+ WHERE f.base_slot < $2 AND f.leaf_slot >= $1
182+ ) AS combined_forks
183+ ORDER BY base_slot ASC, fork_id ASC
184+ ` , startSlot , endSlot )
185+ if err != nil {
186+ return nil , err
187+ }
188+
189+ return forks , nil
190+ }
191+
192+ // GetForkBlockCounts returns the number of blocks for each fork ID
193+ func GetForkBlockCounts (startSlot uint64 , endSlot uint64 ) (map [uint64 ]uint64 , error ) {
194+ args := []any {startSlot , endSlot }
195+
196+ query := `
197+ SELECT fork_id, COUNT(*) as block_count
198+ FROM slots
199+ WHERE status != 0
200+ AND slot >= $1 AND slot < $2
201+ GROUP BY fork_id
202+ `
203+
204+ rows , err := ReaderDb .Query (query , args ... )
205+ if err != nil {
206+ return nil , err
207+ }
208+ defer rows .Close ()
209+
210+ blockCounts := make (map [uint64 ]uint64 )
211+ for rows .Next () {
212+ var forkId , count uint64
213+ if err := rows .Scan (& forkId , & count ); err != nil {
214+ return nil , err
215+ }
216+ blockCounts [forkId ] = count
217+ }
218+
219+ return blockCounts , nil
220+ }
221+
222+ func GetSyncCommitteeParticipation (slot uint64 ) (float64 , error ) {
223+ var participation float64
224+
225+ // Handle potential overflow by using int64
226+ startSlot := int64 (slot ) - 1800
227+ endSlot := int64 (slot ) + 1800
228+
229+ // Ensure we don't go below 0
230+ if startSlot < 0 {
231+ startSlot = 0
232+ }
233+
234+ err := ReaderDb .Get (& participation , `
235+ SELECT
236+ COALESCE(AVG(CASE WHEN sync_participation IS NOT NULL
237+ THEN sync_participation ELSE 0 END), 0) as avg_participation
238+ FROM slots
239+ WHERE slot >= $1 AND slot < $2
240+ AND sync_participation IS NOT NULL
241+ ` , startSlot , endSlot )
242+ if err != nil {
243+ return 0 , err
244+ }
245+
246+ return participation , nil
247+ }
248+
249+ // ForkParticipationByEpoch represents participation data for a fork in a specific epoch
250+ type ForkParticipationByEpoch struct {
251+ ForkId uint64 `db:"fork_id"`
252+ Epoch uint64 `db:"epoch"`
253+ Participation float64 `db:"avg_participation"`
254+ SlotCount uint64 `db:"slot_count"`
255+ }
256+
257+ // GetForkParticipationByEpoch gets average participation per fork per epoch for the given epoch range
258+ // This is optimized to fetch all data in one query to avoid expensive repeated queries
259+ func GetForkParticipationByEpoch (startEpoch , endEpoch uint64 , forkIds []uint64 ) ([]* ForkParticipationByEpoch , error ) {
260+ if len (forkIds ) == 0 {
261+ return []* ForkParticipationByEpoch {}, nil
262+ }
263+
264+ // Build IN clause for fork IDs
265+ var forkPlaceholders strings.Builder
266+ args := make ([]interface {}, 0 , len (forkIds )+ 2 )
267+
268+ for i , forkId := range forkIds {
269+ if i > 0 {
270+ forkPlaceholders .WriteString ("," )
271+ }
272+ args = append (args , forkId )
273+ fmt .Fprintf (& forkPlaceholders , "$%d" , len (args ))
274+ }
275+
276+ // Add epoch range parameters
277+ args = append (args , startEpoch , endEpoch )
278+ startEpochParam := len (args ) - 1
279+ endEpochParam := len (args )
280+
281+ query := fmt .Sprintf (`
282+ SELECT
283+ s.fork_id,
284+ (s.slot / 32) AS epoch,
285+ AVG(COALESCE(s.sync_participation, 0)) AS avg_participation,
286+ COUNT(*) AS slot_count
287+ FROM slots s
288+ WHERE s.fork_id IN (%s)
289+ AND (s.slot / 32) >= $%d AND (s.slot / 32) <= $%d
290+ AND s.sync_participation IS NOT NULL
291+ GROUP BY s.fork_id, (s.slot / 32)
292+ ORDER BY s.fork_id, (s.slot / 32)
293+ ` , forkPlaceholders .String (), startEpochParam , endEpochParam )
294+
295+ var results []* ForkParticipationByEpoch
296+ err := ReaderDb .Select (& results , query , args ... )
297+ if err != nil {
298+ return nil , fmt .Errorf ("error fetching fork participation by epoch: %w" , err )
299+ }
300+
301+ return results , nil
302+ }
0 commit comments