Tournament & System DAOs Reference

Consolidated reference for tournament and system infrastructure DAOs.


Overview

These DAOs handle tournament data and system infrastructure for cloud sync and user management.

DAOs Covered:

Tournament DAOs (3):

  • TournamentDao
  • TournamentParticipantDao
  • TournamentScoreDao

System DAOs (3):

  • UserDao
  • SyncQueueDao
  • ConflictDao

Tournament DAOs

TournamentDao

File: data/dao/TournamentDao.kt Table: tournaments Purpose: Tournament metadata and configuration

Schema

@Entity(tableName = "tournaments")
data class Tournament(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val name: String,
    val location: String,
    val startDate: Long,
    val endDate: Long,
    val format: String,          // TournamentFormat enum
    val organizer: String,
    val status: String,          // PLANNED, IN_PROGRESS, COMPLETED, CANCELLED
    val liveScoringEnabled: Boolean = false,
    val createdAt: Long = System.currentTimeMillis(),
    val modifiedAt: Long = System.currentTimeMillis(),
    val syncStatus: String = "SYNCED"
)

API

@Dao
interface TournamentDao {
    // Insert/Update
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(tournament: Tournament): Long
 
    @Update
    suspend fun update(tournament: Tournament)
 
    @Upsert
    suspend fun upsert(tournament: Tournament)
 
    // Query
    @Query("SELECT * FROM tournaments WHERE id = :tournamentId")
    suspend fun getById(tournamentId: String): Tournament?
 
    @Query("SELECT * FROM tournaments WHERE status = :status ORDER BY startDate DESC")
    suspend fun getByStatus(status: String): List<Tournament>
 
    @Query("SELECT * FROM tournaments WHERE startDate >= :startDate AND endDate <= :endDate")
    suspend fun getInDateRange(startDate: Long, endDate: Long): List<Tournament>
 
    @Query("SELECT * FROM tournaments ORDER BY startDate DESC")
    fun observeAll(): Flow<List<Tournament>>
 
    @Query("SELECT * FROM tournaments WHERE id = :tournamentId")
    fun observe(tournamentId: String): Flow<Tournament?>
 
    // Delete
    @Delete
    suspend fun delete(tournament: Tournament)
 
    @Query("DELETE FROM tournaments WHERE id = :tournamentId")
    suspend fun deleteById(tournamentId: String)
 
    // Sync queries
    @Query("SELECT * FROM tournaments WHERE syncStatus != 'SYNCED'")
    suspend fun getUnsyncedTournaments(): List<Tournament>
 
    @Query("UPDATE tournaments SET syncStatus = :status WHERE id = :tournamentId")
    suspend fun updateSyncStatus(tournamentId: String, status: String)
}

Usage Example

// Create tournament
val tournament = Tournament(
    name = "Spring Championship 2025",
    location = "National Archery Center",
    startDate = parseDate("2025-05-15"),
    endDate = parseDate("2025-05-17"),
    format = "WA_720",
    organizer = "State Archery Association",
    status = "PLANNED"
)
 
tournamentDao.insert(tournament)
 
// Query upcoming tournaments
val upcoming = tournamentDao.getByStatus("PLANNED")
 
// Start tournament
val started = tournament.copy(status = "IN_PROGRESS")
tournamentDao.update(started)
 
// Observe tournament changes
tournamentDao.observe(tournamentId)
    .collect { tournament ->
        updateUI(tournament)
    }

TournamentParticipantDao

File: data/dao/TournamentParticipantDao.kt Table: tournament_participants Purpose: Participant registration and details

Schema

@Entity(
    tableName = "tournament_participants",
    foreignKeys = [
        ForeignKey(
            entity = Tournament::class,
            parentColumns = ["id"],
            childColumns = ["tournamentId"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index("tournamentId")]
)
data class TournamentParticipant(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val tournamentId: String,
    val name: String,
    val division: String,       // Division enum
    val ageCategory: String,    // AgeCategory enum
    val club: String? = null,
    val email: String? = null,
    val targetNumber: String? = null,
    val registeredAt: Long = System.currentTimeMillis()
)

API

@Dao
interface TournamentParticipantDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(participant: TournamentParticipant): Long
 
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(participants: List<TournamentParticipant>)
 
    @Update
    suspend fun update(participant: TournamentParticipant)
 
    @Delete
    suspend fun delete(participant: TournamentParticipant)
 
    // Query by tournament
    @Query("SELECT * FROM tournament_participants WHERE tournamentId = :tournamentId")
    suspend fun getForTournament(tournamentId: String): List<TournamentParticipant>
 
    @Query("SELECT * FROM tournament_participants WHERE tournamentId = :tournamentId AND division = :division")
    suspend fun getForTournamentAndDivision(tournamentId: String, division: String): List<TournamentParticipant>
 
    // Query by ID
    @Query("SELECT * FROM tournament_participants WHERE id = :participantId")
    suspend fun getById(participantId: String): TournamentParticipant?
 
    // Observe
    @Query("SELECT * FROM tournament_participants WHERE tournamentId = :tournamentId")
    fun observeForTournament(tournamentId: String): Flow<List<TournamentParticipant>>
 
    // Count
    @Query("SELECT COUNT(*) FROM tournament_participants WHERE tournamentId = :tournamentId")
    suspend fun getParticipantCount(tournamentId: String): Int
 
    // Search
    @Query("SELECT * FROM tournament_participants WHERE tournamentId = :tournamentId AND name LIKE '%' || :query || '%'")
    suspend fun searchParticipants(tournamentId: String, query: String): List<TournamentParticipant>
}

Usage Example

// Add participant
val participant = TournamentParticipant(
    tournamentId = tournamentId,
    name = "Alice Johnson",
    division = "RECURVE_WOMEN",
    ageCategory = "SENIOR",
    club = "City Archers",
    targetNumber = "1A"
)
 
participantDao.insert(participant)
 
// Bulk add
val participants = listOf(/* ... */)
participantDao.insertAll(participants)
 
// Get all participants for tournament
val allParticipants = participantDao.getForTournament(tournamentId)
 
// Get by division
val recurveWomen = participantDao.getForTournamentAndDivision(
    tournamentId = tournamentId,
    division = "RECURVE_WOMEN"
)
 
// Search
val searchResults = participantDao.searchParticipants(tournamentId, "Alice")

TournamentScoreDao

File: data/dao/TournamentScoreDao.kt Table: tournament_scores Purpose: Tournament scoring data

Schema

@Entity(
    tableName = "tournament_scores",
    foreignKeys = [
        ForeignKey(
            entity = Tournament::class,
            parentColumns = ["id"],
            childColumns = ["tournamentId"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = TournamentParticipant::class,
            parentColumns = ["id"],
            childColumns = ["participantId"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index("tournamentId"), Index("participantId")]
)
data class TournamentScore(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val tournamentId: String,
    val participantId: String,
    val endScoresJson: String,  // Serialized List<EndScore>
    val total: Int,
    val xCount: Int,
    val timestamp: Long = System.currentTimeMillis(),
    val syncStatus: String = "SYNCED"
)
 
// Used for serialization
data class EndScore(
    val endNumber: Int,
    val arrows: List<Int>
)

API

@Dao
interface TournamentScoreDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(score: TournamentScore): Long
 
    @Update
    suspend fun update(score: TournamentScore)
 
    @Delete
    suspend fun delete(score: TournamentScore)
 
    // Query by tournament
    @Query("SELECT * FROM tournament_scores WHERE tournamentId = :tournamentId ORDER BY total DESC")
    suspend fun getForTournament(tournamentId: String): List<TournamentScore>
 
    // Query by participant
    @Query("SELECT * FROM tournament_scores WHERE participantId = :participantId")
    suspend fun getForParticipant(participantId: String): List<TournamentScore>
 
    @Query("SELECT * FROM tournament_scores WHERE tournamentId = :tournamentId AND participantId = :participantId")
    suspend fun getForTournamentAndParticipant(
        tournamentId: String,
        participantId: String
    ): TournamentScore?
 
    // Leaderboard
    @Query("""
        SELECT ts.*, tp.name, tp.division
        FROM tournament_scores ts
        JOIN tournament_participants tp ON ts.participantId = tp.id
        WHERE ts.tournamentId = :tournamentId
        ORDER BY ts.total DESC, ts.xCount DESC
    """)
    suspend fun getLeaderboard(tournamentId: String): List<LeaderboardEntry>
 
    // Observe
    @Query("SELECT * FROM tournament_scores WHERE tournamentId = :tournamentId ORDER BY total DESC")
    fun observeForTournament(tournamentId: String): Flow<List<TournamentScore>>
 
    // Statistics
    @Query("SELECT AVG(total) FROM tournament_scores WHERE tournamentId = :tournamentId")
    suspend fun getAverageScore(tournamentId: String): Double?
 
    @Query("SELECT MAX(total) FROM tournament_scores WHERE tournamentId = :tournamentId")
    suspend fun getHighScore(tournamentId: String): Int?
 
    // Sync
    @Query("SELECT * FROM tournament_scores WHERE syncStatus != 'SYNCED'")
    suspend fun getUnsyncedScores(): List<TournamentScore>
}
 
// Result class for leaderboard query
data class LeaderboardEntry(
    val id: String,
    val tournamentId: String,
    val participantId: String,
    val endScoresJson: String,
    val total: Int,
    val xCount: Int,
    val timestamp: Long,
    val syncStatus: String,
    val name: String,
    val division: String
)

Usage Example

// Submit score
val score = TournamentScore(
    tournamentId = tournamentId,
    participantId = participantId,
    endScoresJson = Json.encodeToString(endScores),
    total = 654,
    xCount = 45
)
 
scoreDao.insert(score)
 
// Get leaderboard
val leaderboard = scoreDao.getLeaderboard(tournamentId)
 
leaderboard.forEachIndexed { index, entry ->
    println("${index + 1}. ${entry.name}: ${entry.total} (${entry.xCount}X)")
}
 
// Real-time updates
scoreDao.observeForTournament(tournamentId)
    .collect { scores ->
        updateLeaderboardUI(scores)
    }

System DAOs

UserDao

File: data/dao/UserDao.kt Table: users Purpose: User profile and preferences

Schema

@Entity(tableName = "users")
data class User(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val firebaseUid: String? = null,
    val email: String,
    val displayName: String,
    val photoUrl: String? = null,
    val club: String? = null,
    val division: String? = null,
    val ageCategory: String? = null,
    val createdAt: Long = System.currentTimeMillis(),
    val lastLoginAt: Long = System.currentTimeMillis(),
    val preferences: String = "{}"  // JSON serialized preferences
)

API

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User): Long
 
    @Update
    suspend fun update(user: User)
 
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getById(userId: String): User?
 
    @Query("SELECT * FROM users WHERE firebaseUid = :firebaseUid")
    suspend fun getByFirebaseUid(firebaseUid: String): User?
 
    @Query("SELECT * FROM users WHERE email = :email")
    suspend fun getByEmail(email: String): User?
 
    @Query("SELECT * FROM users LIMIT 1")
    suspend fun getCurrentUser(): User?
 
    @Query("UPDATE users SET lastLoginAt = :timestamp WHERE id = :userId")
    suspend fun updateLastLogin(userId: String, timestamp: Long)
 
    @Query("UPDATE users SET preferences = :preferencesJson WHERE id = :userId")
    suspend fun updatePreferences(userId: String, preferencesJson: String)
 
    @Delete
    suspend fun delete(user: User)
 
    fun observeCurrentUser(): Flow<User?>
}

Usage Example

// Create user on first login
val user = User(
    firebaseUid = firebaseAuth.currentUser?.uid,
    email = "archer@example.com",
    displayName = "Alice Archer",
    club = "City Archers",
    division = "RECURVE_WOMEN"
)
 
userDao.insert(user)
 
// Update preferences
val preferences = Preferences(
    theme = "dark",
    units = "metric",
    notifications = true
)
 
userDao.updatePreferences(
    userId = user.id,
    preferencesJson = Json.encodeToString(preferences)
)
 
// Get current user
val currentUser = userDao.getCurrentUser()

SyncQueueDao

File: data/dao/SyncQueueDao.kt Table: sync_queue Purpose: Queue pending sync operations for offline support

Schema

@Entity(tableName = "sync_queue")
data class SyncQueueItem(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val entityType: String,      // "ROUND", "TOURNAMENT_SCORE", etc.
    val entityId: String,
    val operation: String,       // "CREATE", "UPDATE", "DELETE"
    val dataJson: String,        // Serialized entity data
    val timestamp: Long = System.currentTimeMillis(),
    val retryCount: Int = 0,
    val lastAttempt: Long? = null,
    val error: String? = null
)

API

@Dao
interface SyncQueueDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(item: SyncQueueItem): Long
 
    @Insert
    suspend fun insertAll(items: List<SyncQueueItem>)
 
    @Update
    suspend fun update(item: SyncQueueItem)
 
    @Delete
    suspend fun delete(item: SyncQueueItem)
 
    // Get pending items
    @Query("SELECT * FROM sync_queue ORDER BY timestamp ASC")
    suspend fun getAllPending(): List<SyncQueueItem>
 
    @Query("SELECT * FROM sync_queue WHERE entityType = :entityType")
    suspend fun getPendingForEntityType(entityType: String): List<SyncQueueItem>
 
    @Query("SELECT COUNT(*) FROM sync_queue")
    suspend fun getPendingCount(): Int
 
    // Retry management
    @Query("SELECT * FROM sync_queue WHERE retryCount < :maxRetries")
    suspend fun getPendingWithRetries(maxRetries: Int = 3): List<SyncQueueItem>
 
    @Query("UPDATE sync_queue SET retryCount = retryCount + 1, lastAttempt = :timestamp, error = :error WHERE id = :itemId")
    suspend fun incrementRetry(itemId: String, timestamp: Long, error: String)
 
    // Clean up
    @Query("DELETE FROM sync_queue WHERE id = :itemId")
    suspend fun deleteById(itemId: String)
 
    @Query("DELETE FROM sync_queue WHERE retryCount >= :maxRetries")
    suspend fun deleteFailedItems(maxRetries: Int = 5)
 
    // Observe
    fun observePendingCount(): Flow<Int>
}

Usage Example

// Queue item for sync when offline
val queueItem = SyncQueueItem(
    entityType = "TOURNAMENT_SCORE",
    entityId = score.id,
    operation = "CREATE",
    dataJson = Json.encodeToString(score)
)
 
syncQueueDao.insert(queueItem)
 
// Process queue when online
val pendingItems = syncQueueDao.getAllPending()
 
pendingItems.forEach { item ->
    try {
        // Sync to cloud
        firebaseService.sync(item)
        syncQueueDao.delete(item)
    } catch (e: Exception) {
        // Increment retry count
        syncQueueDao.incrementRetry(
            itemId = item.id,
            timestamp = System.currentTimeMillis(),
            error = e.message ?: "Unknown error"
        )
    }
}
 
// Show pending count in UI
syncQueueDao.observePendingCount()
    .collect { count ->
        updateBadge(count)
    }

ConflictDao

File: data/dao/ConflictDao.kt Table: conflicts Purpose: Track and resolve sync conflicts

Schema

@Entity(tableName = "conflicts")
data class DataConflict(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val entityType: String,
    val entityId: String,
    val conflictType: String,    // "BOTH_MODIFIED", "LOCAL_DELETED", etc.
    val localDataJson: String?,
    val remoteDataJson: String?,
    val localTimestamp: Long,
    val remoteTimestamp: Long,
    val detectedAt: Long = System.currentTimeMillis(),
    val resolvedAt: Long? = null,
    val resolution: String? = null,  // "KEEP_LOCAL", "KEEP_REMOTE", "MERGE"
    val resolvedBy: String? = null   // User ID or "auto"
)

API

@Dao
interface ConflictDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(conflict: DataConflict): Long
 
    @Insert
    suspend fun insertAll(conflicts: List<DataConflict>)
 
    @Update
    suspend fun update(conflict: DataConflict)
 
    @Delete
    suspend fun delete(conflict: DataConflict)
 
    // Query pending conflicts
    @Query("SELECT * FROM conflicts WHERE resolvedAt IS NULL ORDER BY detectedAt DESC")
    suspend fun getPendingConflicts(): List<DataConflict>
 
    @Query("SELECT * FROM conflicts WHERE resolvedAt IS NULL AND entityType = :entityType")
    suspend fun getPendingConflictsForEntityType(entityType: String): List<DataConflict>
 
    @Query("SELECT COUNT(*) FROM conflicts WHERE resolvedAt IS NULL")
    suspend fun getPendingConflictCount(): Int
 
    // Resolve conflict
    @Query("""
        UPDATE conflicts
        SET resolvedAt = :timestamp, resolution = :resolution, resolvedBy = :resolvedBy
        WHERE id = :conflictId
    """)
    suspend fun resolveConflict(
        conflictId: String,
        resolution: String,
        resolvedBy: String,
        timestamp: Long
    )
 
    // History
    @Query("SELECT * FROM conflicts WHERE entityId = :entityId ORDER BY detectedAt DESC")
    suspend fun getConflictHistory(entityId: String): List<DataConflict>
 
    @Query("SELECT * FROM conflicts WHERE resolvedAt IS NOT NULL ORDER BY resolvedAt DESC LIMIT :limit")
    suspend fun getRecentlyResolved(limit: Int = 10): List<DataConflict>
 
    // Clean up old resolved conflicts
    @Query("DELETE FROM conflicts WHERE resolvedAt IS NOT NULL AND resolvedAt < :timestamp")
    suspend fun deleteOldResolved(timestamp: Long)
 
    // Observe
    @Query("SELECT COUNT(*) FROM conflicts WHERE resolvedAt IS NULL")
    fun observePendingCount(): Flow<Int>
}

Usage Example

// Detect and store conflict
val conflict = DataConflict(
    entityType = "ROUND",
    entityId = round.id.toString(),
    conflictType = "BOTH_MODIFIED",
    localDataJson = Json.encodeToString(localRound),
    remoteDataJson = Json.encodeToString(remoteRound),
    localTimestamp = localRound.modifiedAt,
    remoteTimestamp = remoteRound.modifiedAt
)
 
conflictDao.insert(conflict)
 
// Get pending conflicts
val pending = conflictDao.getPendingConflicts()
 
// Show conflict resolution UI
pending.forEach { conflict ->
    showConflictDialog(conflict) { resolution ->
        conflictDao.resolveConflict(
            conflictId = conflict.id,
            resolution = resolution.strategy,
            resolvedBy = currentUser.id,
            timestamp = System.currentTimeMillis()
        )
    }
}
 
// Monitor conflicts
conflictDao.observePendingCount()
    .collect { count ->
        if (count > 0) {
            showConflictBadge(count)
        }
    }

Testing

Example Tests

@Test
fun insertAndRetrieveTournament() = runTest {
    val tournament = Tournament(
        name = "Test Tournament",
        location = "Test Location",
        startDate = System.currentTimeMillis(),
        endDate = System.currentTimeMillis() + 86400000,
        format = "WA_720",
        organizer = "Test Org",
        status = "PLANNED"
    )
 
    tournamentDao.insert(tournament)
    val retrieved = tournamentDao.getById(tournament.id)
 
    assertEquals(tournament.name, retrieved?.name)
}
 
@Test
fun getLeaderboardSortedByScore() = runTest {
    // Insert participants and scores
    val p1 = TournamentParticipant(id = "p1", tournamentId = "t1", name = "Alice", ...)
    val p2 = TournamentParticipant(id = "p2", tournamentId = "t1", name = "Bob", ...)
 
    participantDao.insertAll(listOf(p1, p2))
 
    val s1 = TournamentScore(tournamentId = "t1", participantId = "p1", total = 650, xCount = 40)
    val s2 = TournamentScore(tournamentId = "t1", participantId = "p2", total = 680, xCount = 45)
 
    scoreDao.insert(s1)
    scoreDao.insert(s2)
 
    val leaderboard = scoreDao.getLeaderboard("t1")
 
    assertEquals("Bob", leaderboard[0].name)  // Higher score first
    assertEquals(680, leaderboard[0].total)
}
 
@Test
fun syncQueueProcessing() = runTest {
    val item = SyncQueueItem(
        entityType = "ROUND",
        entityId = "r1",
        operation = "CREATE",
        dataJson = "{}"
    )
 
    syncQueueDao.insert(item)
    assertEquals(1, syncQueueDao.getPendingCount())
 
    syncQueueDao.deleteById(item.id)
    assertEquals(0, syncQueueDao.getPendingCount())
}


Status: ✅ All 6 DAOs in production Pattern: Consistent Room DAO with offline-first support Test Coverage: Integration tests for all DAOs Last Updated: 2025-11-01