Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions CaPPMS/Data/DBOperationsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Humanizer;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Octokit;
using System;
using System.Collections.Generic;
using System.Data;
Expand All @@ -21,12 +22,10 @@ public class DBOperationsService
private const string ConnectionStringFormat = @"Data Source={0}";
private const string RetriveStudentScoreDetailsFileName = "ReadStudentScoreDetails.sql";
private const string ReadStudentScoresFileName = "ReadStudentScores.sql";
private const string ReadStudentScoreByStudentFileName = "ReadStudentScoreByStudent.sql";
private const string StudentDataBaseCreation = "StudentDataBaseCreation.sql";

private const string TableNameNotFoundMessage = "Table name not found for type {0}. Skipping verification.";
private static string readStudentScoreDetails;
private static string readStudentScore;
private static string readStudentScoreById;

private static volatile int dbBroker;
private static readonly TimeSpan brokerTimeout = TimeSpan.FromSeconds(10);
Expand All @@ -35,7 +34,6 @@ static DBOperationsService()
{
readStudentScoreDetails = GetResourceData(RetriveStudentScoreDetailsFileName);
readStudentScore = GetResourceData(ReadStudentScoresFileName);
readStudentScoreById = GetResourceData(ReadStudentScoreByStudentFileName);
}

private string connectionString;
Expand Down Expand Up @@ -416,6 +414,10 @@ public async Task EnsureDbExistsAsync()
}
}

// Verify database.
// Can be that the database is created but the backing tables have changed.
await VerifiyDatabase();

dbBroker = 0;
}

Expand Down Expand Up @@ -532,5 +534,112 @@ private async Task CreateDbAsync()
this.logger.LogError($"Error creating database. Error:{ex.GetBaseException()}");
}
}

private async Task VerifiyDatabase()
{
Type[] dbTypes = [.. Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => type.GetCustomAttribute<SqlTableNameAttribute>() != null)];

foreach (Type type in dbTypes)
{
string tableName = ReflectionHelper.GetTableName(type);
if (string.IsNullOrEmpty(tableName))
{
logger.LogWarning(TableNameNotFoundMessage, type.Name);
continue;
}

bool result = await VerifyColumnsExistAsync(type, tableName);
if (result)
{
logger.LogDebug($"Table '{tableName}' verification succeeded for type '{type.Name}'.");
}
else
{
logger.LogWarning($"Table '{tableName}' verification failed for type '{type.Name}'.");
}
}
}

private async Task<bool> VerifyColumnsExistAsync(Type type, string tableName)
{
var properties = ReflectionHelper.GetNonIgnoredProperties(type);
var columnNames = await GetColumnNamesFromDatabaseAsync(tableName);

foreach (var property in properties)
{
if (!columnNames.Contains(property.Name))
{
logger.LogWarning($"Column '{property.Name}' does not exist in table '{tableName}'. Attempting to add it.");
bool columnAdded = await TryAddColumnAsync(tableName, property);
if (!columnAdded)
{
logger.LogError($"Failed to add column '{property.Name}' to table '{tableName}'.");
return false;
}
}
}

return true;
}

private async Task<bool> TryAddColumnAsync(string tableName, PropertyInfo property)
{
string columnType = GetSqlType(property.PropertyType);
string query = $"ALTER TABLE {tableName} ADD COLUMN {property.Name} {columnType};";

try
{
await ExecuteNonQueryAsync(query);
return true;
}
catch (Exception ex)
{
logger.LogError($"Error adding column '{property.Name}' to table '{tableName}': {ex.Message}");
return false;
}
}

private string GetSqlType(Type type)
{
if (type == typeof(int) || type == typeof(long))
{
return "INTEGER";
}
if (type == typeof(string) || type == typeof(IEnumerable<string>))
{
return "TEXT";
}
if (type == typeof(bool))
{
return "BOOLEAN";
}
if (type == typeof(double) || type == typeof(float))
{
return "REAL";
}
if (type == typeof(DateTime))
{
return "DATETIME";
}

// Add more type mappings as needed
throw new NotSupportedException($"Type '{type.Name}' is not supported.");
}

private async Task<HashSet<string>> GetColumnNamesFromDatabaseAsync(string tableName)
{
var columnNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var query = $"PRAGMA table_info({tableName})";

await this.ExecuteQueryAsync(query, (reader, map) =>
{
columnNames.Add(reader.GetString(1)); // Column name is in the second column
});

return columnNames;
}
}
}
6 changes: 6 additions & 0 deletions CaPPMS/Extensions/IDataReaderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ private static object ConvertValue(object value, Type expectedType)
return Convert.ToDouble(value);
}

// Check booleans
if (expectedType == typeof(bool))
{
return Convert.ToBoolean(value);
}

// Check lists
if (expectedType == typeof(List<string>))
{
Expand Down
22 changes: 22 additions & 0 deletions CaPPMS/Helpers/ReflectionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using CaPPMS.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

public static class ReflectionHelper
{
public static IEnumerable<PropertyInfo> GetNonIgnoredProperties(Type type)
{
return type.GetProperties()
.Where(prop => !Attribute.IsDefined(prop, typeof(IgnoreDataMemberAttribute)));
}

public static string GetTableName(Type type)
{
var tableNameAttribute = type.GetCustomAttributes(typeof(SqlTableNameAttribute), true)
.FirstOrDefault() as SqlTableNameAttribute;
return tableNameAttribute?.TableName ?? string.Empty;
}
}
39 changes: 39 additions & 0 deletions CaPPMS/Model/Student.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,55 @@

namespace CaPPMS.Model
{
/// <summary>
/// Model for the Student.
/// </summary>
[SqlTableName("Students")]
public class Student : ISqlTableModel
{
private const string EmailSuffix = "@student.umgc.edu";

/// <summary>
/// Initialize a new instance of the <see cref="Student"/> class.
/// </summary>
public Student() { }

/// <summary>
/// Gets or sets the student ID.
/// </summary>
[Required]
[SqlIdProperty]
public long StudentId { get; set; } = -1;

/// <summary>
/// Gets or sets the first name.
/// </summary>
[ColumnHeader]
[DisplayName("First Name")]
public string FirstName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the last name.
/// </summary>
[ColumnHeader]
[DisplayName("Last Name")]
public string LastName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the email address.
/// </summary>
[ColumnHeader]
public string Email { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the GitHub account.
/// </summary>
[ColumnHeader]
public string GitHub { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the team name.
/// </summary>
[ColumnHeader]
[DisplayName("Team")]
[IgnoreDataMember]
Expand All @@ -49,9 +73,15 @@ public string? TeamName
}
}

/// <summary>
/// Gets or sets the team assigned to the student.
/// </summary>
[IgnoreDataMember]
public Team AssignedTeam { get; private set; } = new Team();

/// <summary>
/// Gets or sets the team ID.
/// </summary>
public long TeamId
{
get
Expand All @@ -64,8 +94,17 @@ public long TeamId
}
}

/// <summary>
/// Gets or sets the class ID.
/// </summary>
public long ClassId { get; set; } = -1;

/// <summary>
/// Gets or sets a value indicating whether the student is a team leader.
/// </summary>
[ColumnHeader]
public bool IsTeamLead { get; set; } = false;

/// <summary>
/// Set the team for the student.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion CaPPMS/Model/StudentScores.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using CaPPMS.Attributes;
using System.Collections.Generic;

namespace CaPPMS.Model
{
[SqlTableName(tableName: "")]
public class StudentScores : Student
{
public StudentScores() { }
Expand Down
Loading