Utopia-Canteen-System/Data/AppDbContext.cs

688 lines
28 KiB
C#

using System.Data;
using System.Data.Common;
using System.IO;
using Microsoft.EntityFrameworkCore;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Data;
/// <summary>
/// SQLite DbContext for Labour and lunch_order_transactions (scan records) tables.
/// Database file is stored under LocalApplicationData so it persists across app updates
/// when deployed from a file server.
/// </summary>
public class AppDbContext : DbContext
{
private static string DbPath => DatabasePath.GetDbPath();
public DbSet<Labour> Labour { get; set; }
public DbSet<ScanRecord> LunchOrderTransactions { get; set; }
public DbSet<AdminLoginRecord> AdminLoginRecords { get; set; }
public DbSet<EmployeeRfidTagCache> EmployeeRfidTagCache { get; set; }
public DbSet<MealScheduleCache> MealScheduleCache { get; set; }
public DbSet<LunchMenuWeekCache> LunchMenuWeekCache { get; set; }
public DbSet<LunchMenuItemCache> LunchMenuItemCache { get; set; }
public DbSet<MenuItemCache> MenuItemCache { get; set; }
public AppDbContext() { }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlite($"Data Source={DbPath}");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Labour: CardId should be indexed for lookups.
modelBuilder.Entity<Labour>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.CardId);
});
// lunch_order_transactions (scan records): Index for unsynced queries and by ScanTime.
modelBuilder.Entity<ScanRecord>(e =>
{
e.ToTable("lunch_order_transactions");
e.HasKey(x => x.Id);
e.HasIndex(x => x.IsSynced);
e.HasIndex(x => x.ScanTime);
});
modelBuilder.Entity<AdminLoginRecord>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.LoginTimeUtc);
});
modelBuilder.Entity<EmployeeRfidTagCache>(e =>
{
e.ToTable("employee_rfid_tag_cache");
e.HasKey(x => x.LocalId);
e.HasIndex(x => x.HrmsId).IsUnique();
e.HasIndex(x => x.ManufacturerSerial);
});
modelBuilder.Entity<MealScheduleCache>(e =>
{
e.ToTable("meal_schedule_cache");
e.HasKey(x => x.LocalId);
e.HasIndex(x => x.HrmsId).IsUnique();
e.HasIndex(x => x.LocationSiteId);
});
modelBuilder.Entity<LunchMenuWeekCache>(e =>
{
e.ToTable("lunch_menu_week_cache");
e.HasKey(x => x.LocalId);
e.HasIndex(x => x.HrmsId).IsUnique();
e.HasIndex(x => x.LocationSiteId);
});
modelBuilder.Entity<LunchMenuItemCache>(e =>
{
e.ToTable("lunch_menu_item_cache");
e.HasKey(x => x.LocalId);
e.HasIndex(x => x.HrmsId).IsUnique();
e.HasIndex(x => x.LunchMenuWeekHrmsId);
e.HasIndex(x => x.MenuDate);
});
modelBuilder.Entity<MenuItemCache>(e =>
{
e.ToTable("menu_item_cache");
e.HasKey(x => x.LocalId);
e.HasIndex(x => x.HrmsId).IsUnique();
});
}
/// <summary>
/// Ensures database exists and is migrated. Call on app startup.
/// </summary>
public void EnsureDatabaseCreated()
{
Database.EnsureCreated();
MigrateScanRecordsToLunchOrderTransactionsIfNeeded();
UpgradeLunchOrderTransactionsSchemaIfNeeded();
EnsureAdminLoginTableExists();
EnsureEmployeeRfidTagCacheTableExists();
EnsureMealMenuCacheTablesExist();
RemoveLegacyMealRelatedSchemaIfNeeded();
}
/// <summary>
/// One-time migration: rename old tables (ScanRecords or CanteenScanEvents) to lunch_order_transactions for existing databases.
/// </summary>
private void MigrateScanRecordsToLunchOrderTransactionsIfNeeded()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var hasOldScanRecords = false;
var hasOldCanteenScanEvents = false;
var hasNew = false;
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='ScanRecords'";
using var r = cmd.ExecuteReader();
hasOldScanRecords = r.Read();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='CanteenScanEvents'";
using var r = cmd.ExecuteReader();
hasOldCanteenScanEvents = r.Read();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='lunch_order_transactions'";
using var r = cmd.ExecuteReader();
hasNew = r.Read();
}
if (!hasNew)
{
if (hasOldScanRecords)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords RENAME TO lunch_order_transactions";
cmd.ExecuteNonQuery();
}
else if (hasOldCanteenScanEvents)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE CanteenScanEvents RENAME TO lunch_order_transactions";
cmd.ExecuteNonQuery();
}
}
}
catch
{
// Ignore; DB may be new or already migrated
}
}
/// <summary>
/// Lightweight schema upgrade: add SiteId, DeviceId and IpAddress to lunch_order_transactions if missing (no EF migrations).
/// Does not delete any data.
/// </summary>
private void UpgradeLunchOrderTransactionsSchemaIfNeeded()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var columns = new List<string>();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM pragma_table_info('lunch_order_transactions')";
using var r = cmd.ExecuteReader();
while (r.Read())
columns.Add(r.GetString(0));
}
if (columns.Count > 0 && !columns.Contains("SiteId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN SiteId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DeviceId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("IpAddress", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN IpAddress TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("MealSessionCode", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealSessionCode INTEGER DEFAULT -1";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("ParentDocumentId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN ParentDocumentId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("EmployeeId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN EmployeeId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("UindSerial", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN UindSerial TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("FunctionId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN FunctionId INTEGER DEFAULT 0";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("DepartmentId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DepartmentId INTEGER DEFAULT 0";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("TagCreatedAtUtc", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedAtUtc TEXT";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("TagCreatedBy", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedBy TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("EmployeeName", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN EmployeeName TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("Department", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN Department TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("DepartmentType", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DepartmentType TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("MealLabel", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealLabel TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("MealItems", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealItems TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("TotalPrice", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TotalPrice REAL DEFAULT 0";
cmd.ExecuteNonQuery();
}
// Remove deprecated column if it exists (older installs).
RemoveEmployeeDbIdColumnIfPresent(conn, columns);
}
catch
{
// Ignore; existing DB may already have columns or be incompatible
}
}
/// <summary>
/// Removes deprecated EmployeeDbId column from lunch_order_transactions if present.
/// Uses DROP COLUMN when supported; otherwise rebuilds the table without that column.
/// Best-effort and non-fatal.
/// </summary>
private static void RemoveEmployeeDbIdColumnIfPresent(DbConnection conn, List<string> existingColumns)
{
if (existingColumns.Count == 0 || !existingColumns.Contains("EmployeeDbId", StringComparer.OrdinalIgnoreCase))
return;
// Try modern SQLite DROP COLUMN first.
try
{
using var dropCmd = conn.CreateCommand();
dropCmd.CommandText = "ALTER TABLE lunch_order_transactions DROP COLUMN EmployeeDbId";
dropCmd.ExecuteNonQuery();
return;
}
catch
{
// Fall back to table rebuild.
}
try
{
using var tx = conn.BeginTransaction();
using (var createCmd = conn.CreateCommand())
{
createCmd.Transaction = tx;
createCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS lunch_order_transactions__new (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
CardId TEXT,
ScanTime TEXT,
IsSynced INTEGER,
SiteId TEXT,
DeviceId TEXT,
IpAddress TEXT,
ParentDocumentId TEXT,
EmployeeId TEXT,
UindSerial TEXT,
FunctionId INTEGER,
DepartmentId INTEGER,
TagCreatedAtUtc TEXT,
TagCreatedBy TEXT,
EmployeeName TEXT,
Department TEXT,
DepartmentType TEXT,
MealLabel TEXT,
MealItems TEXT,
TotalPrice REAL
)";
createCmd.ExecuteNonQuery();
}
using (var copyCmd = conn.CreateCommand())
{
copyCmd.Transaction = tx;
copyCmd.CommandText = @"
INSERT INTO lunch_order_transactions__new
(Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice)
SELECT
Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice
FROM lunch_order_transactions";
copyCmd.ExecuteNonQuery();
}
using (var dropOldCmd = conn.CreateCommand())
{
dropOldCmd.Transaction = tx;
dropOldCmd.CommandText = "DROP TABLE lunch_order_transactions";
dropOldCmd.ExecuteNonQuery();
}
using (var renameCmd = conn.CreateCommand())
{
renameCmd.Transaction = tx;
renameCmd.CommandText = "ALTER TABLE lunch_order_transactions__new RENAME TO lunch_order_transactions";
renameCmd.ExecuteNonQuery();
}
// Recreate basic indexes used by the app.
using (var idxCmd = conn.CreateCommand())
{
idxCmd.Transaction = tx;
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_IsSynced ON lunch_order_transactions(IsSynced)";
idxCmd.ExecuteNonQuery();
}
using (var idxCmd = conn.CreateCommand())
{
idxCmd.Transaction = tx;
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_ScanTime ON lunch_order_transactions(ScanTime)";
idxCmd.ExecuteNonQuery();
}
tx.Commit();
}
catch
{
// Best-effort; ignore failures.
}
}
/// <summary>
/// Lightweight creation of AdminLoginRecords table for existing databases.
/// </summary>
private void EnsureAdminLoginTableExists()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS AdminLoginRecords (" +
"Id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"Username TEXT NOT NULL, " +
"EmployeeId TEXT NOT NULL, " +
"LoginTimeUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
}
catch
{
// Ignore; table may already exist or DB may be read-only.
}
}
/// <summary>
/// Lightweight creation of employee_rfid_tag_cache for existing databases (no EF migrations).
/// </summary>
private void EnsureEmployeeRfidTagCacheTableExists()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS employee_rfid_tag_cache (" +
"LocalId INTEGER PRIMARY KEY AUTOINCREMENT, " +
"HrmsId INTEGER NOT NULL, " +
"ManufacturerSerial TEXT NOT NULL DEFAULT '', " +
"UindSerial TEXT NOT NULL DEFAULT '', " +
"Secret TEXT NOT NULL DEFAULT '', " +
"ParentDocumentType TEXT NOT NULL DEFAULT '', " +
"ParentDocumentId TEXT NOT NULL DEFAULT '', " +
"DateTimeCreated TEXT, " +
"CreatedBy TEXT NOT NULL DEFAULT '', " +
"GradeId INTEGER, " +
"GradeType TEXT NOT NULL DEFAULT '', " +
"ShiftId INTEGER, " +
"LocationSiteId TEXT NOT NULL DEFAULT '', " +
"RfidLocationSiteId TEXT NOT NULL DEFAULT '', " +
"FunctionId INTEGER NOT NULL DEFAULT 0, " +
"DepartmentId INTEGER NOT NULL DEFAULT 0, " +
"ReportingManagerIds TEXT NOT NULL DEFAULT '', " +
"EmployeeSerialNumber TEXT NOT NULL DEFAULT '', " +
"EmployeeConcatenatedName TEXT NOT NULL DEFAULT '', " +
"DepartmentTitle TEXT NOT NULL DEFAULT '', " +
"DepartmentType TEXT NOT NULL DEFAULT '', " +
"LastSyncedAtUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
using var idxHrms = conn.CreateCommand();
idxHrms.CommandText =
"CREATE UNIQUE INDEX IF NOT EXISTS IX_employee_rfid_tag_cache_HrmsId ON employee_rfid_tag_cache(HrmsId)";
idxHrms.ExecuteNonQuery();
using var idxSerial = conn.CreateCommand();
idxSerial.CommandText =
"CREATE INDEX IF NOT EXISTS IX_employee_rfid_tag_cache_ManufacturerSerial ON employee_rfid_tag_cache(ManufacturerSerial)";
idxSerial.ExecuteNonQuery();
}
catch
{
// Ignore; table may already exist or DB may be read-only.
}
}
private void EnsureMealMenuCacheTablesExist()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS meal_schedule_cache (" +
"LocalId INTEGER PRIMARY KEY AUTOINCREMENT, " +
"HrmsId INTEGER NOT NULL, " +
"MealName TEXT NOT NULL DEFAULT '', " +
"StartTime TEXT NOT NULL DEFAULT '', " +
"EndTime TEXT NOT NULL DEFAULT '', " +
"CreatedAt TEXT, " +
"UpdatedAt TEXT, " +
"LocationSiteId INTEGER NOT NULL DEFAULT 0, " +
"LastSyncedAtUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS lunch_menu_week_cache (" +
"LocalId INTEGER PRIMARY KEY AUTOINCREMENT, " +
"HrmsId INTEGER NOT NULL, " +
"WeekStartDate TEXT, " +
"WeekEndDate TEXT, " +
"CreatedBy TEXT NOT NULL DEFAULT '', " +
"CreatedAt TEXT, " +
"LocationSiteId INTEGER NOT NULL DEFAULT 0, " +
"LastSyncedAtUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS lunch_menu_item_cache (" +
"LocalId INTEGER PRIMARY KEY AUTOINCREMENT, " +
"HrmsId INTEGER NOT NULL, " +
"LunchMenuWeekHrmsId INTEGER NOT NULL DEFAULT 0, " +
"DayOfWeek TEXT NOT NULL DEFAULT '', " +
"MealName TEXT NOT NULL DEFAULT '', " +
"MenuItemHrmsId INTEGER NOT NULL DEFAULT 0, " +
"CreatedAt TEXT, " +
"MenuDate TEXT, " +
"LastSyncedAtUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"CREATE TABLE IF NOT EXISTS menu_item_cache (" +
"LocalId INTEGER PRIMARY KEY AUTOINCREMENT, " +
"HrmsId INTEGER NOT NULL, " +
"ItemName TEXT NOT NULL DEFAULT '', " +
"ItemType TEXT NOT NULL DEFAULT '', " +
"Price REAL NOT NULL DEFAULT 0, " +
"ItemFor TEXT NOT NULL DEFAULT '', " +
"LocationSiteId INTEGER NOT NULL DEFAULT 0, " +
"LastSyncedAtUtc TEXT NOT NULL)";
cmd.ExecuteNonQuery();
}
CreateIndexIfNotExists(conn, "IX_meal_schedule_cache_HrmsId", "meal_schedule_cache", "HrmsId", unique: true);
CreateIndexIfNotExists(conn, "IX_meal_schedule_cache_LocationSiteId", "meal_schedule_cache", "LocationSiteId", unique: false);
CreateIndexIfNotExists(conn, "IX_lunch_menu_week_cache_HrmsId", "lunch_menu_week_cache", "HrmsId", unique: true);
CreateIndexIfNotExists(conn, "IX_lunch_menu_week_cache_LocationSiteId", "lunch_menu_week_cache", "LocationSiteId", unique: false);
CreateIndexIfNotExists(conn, "IX_lunch_menu_item_cache_HrmsId", "lunch_menu_item_cache", "HrmsId", unique: true);
CreateIndexIfNotExists(conn, "IX_lunch_menu_item_cache_WeekId", "lunch_menu_item_cache", "LunchMenuWeekHrmsId", unique: false);
CreateIndexIfNotExists(conn, "IX_menu_item_cache_HrmsId", "menu_item_cache", "HrmsId", unique: true);
}
catch
{
// Ignore; table may already exist or DB may be read-only.
}
}
private static void CreateIndexIfNotExists(DbConnection conn, string indexName, string tableName, string columnName, bool unique)
{
using var cmd = conn.CreateCommand();
var uniqueSql = unique ? "UNIQUE " : string.Empty;
cmd.CommandText = $"CREATE {uniqueSql}INDEX IF NOT EXISTS {indexName} ON {tableName}({columnName})";
cmd.ExecuteNonQuery();
}
/// <summary>
/// Removes legacy meal-related tables and columns that should never be present in production.
/// This is a best-effort cleanup that runs on every startup for existing SQLite databases.
/// </summary>
private void RemoveLegacyMealRelatedSchemaIfNeeded()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
// 1) Drop whole legacy tables if they exist.
var legacyTables = new[]
{
"Employees",
"MealRates",
// Support both spellings of the temporary meal plan overrides table.
"TemporaryMealPlanOverirdes",
"TemporaryMealPlanOverrides",
// Old local-only tables we no longer use; all meal timings now come from production HRMS.
"MealSchedules",
"OrderItems"
};
foreach (var table in legacyTables)
{
using var dropCmd = conn.CreateCommand();
dropCmd.CommandText = $"DROP TABLE IF EXISTS \"{table}\"";
dropCmd.ExecuteNonQuery();
}
// 2) Strip specific columns from any user tables that might contain them.
// We don't assume which table they belong to; instead we scan all non-system tables.
var legacyColumns = new[]
{
"MealSession",
"MealPlanType",
"AppliedRate",
"TotalAmount"
};
var tableNames = new List<string>();
using (var tablesCmd = conn.CreateCommand())
{
tablesCmd.CommandText =
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
using var reader = tablesCmd.ExecuteReader();
while (reader.Read())
tableNames.Add(reader.GetString(0));
}
foreach (var tableName in tableNames)
{
var existingColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var columnsCmd = conn.CreateCommand())
{
// Use pragma_table_info to discover columns for the current table.
var safeTableName = tableName.Replace("'", "''");
columnsCmd.CommandText = $"SELECT name FROM pragma_table_info('{safeTableName}')";
using var reader = columnsCmd.ExecuteReader();
while (reader.Read())
existingColumns.Add(reader.GetString(0));
}
foreach (var legacyColumn in legacyColumns)
{
if (!existingColumns.Contains(legacyColumn))
continue;
try
{
using var dropColumnCmd = conn.CreateCommand();
dropColumnCmd.CommandText =
$"ALTER TABLE \"{tableName}\" DROP COLUMN \"{legacyColumn}\"";
dropColumnCmd.ExecuteNonQuery();
}
catch
{
// Older SQLite versions may not support DROP COLUMN; ignore in that case.
// If needed in the future, a more invasive recreate-table migration can be added.
}
}
}
}
catch
{
// Best-effort only: if anything fails, do not block app startup.
}
}
}