Skip to content
Merged
Changes from 1 commit
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
130 changes: 66 additions & 64 deletions PortfolioViewer/PortfolioViewer.ApiService/Services/SyncGrpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,7 @@
public class SyncGrpcService(DatabaseContext dbContext, ILogger<SyncGrpcService> logger) : SyncService.SyncServiceBase
{
private readonly string[] _tablesToIgnore = ["sqlite_sequence", "__EFMigrationsHistory", "__EFMigrationsLock"];
private readonly Dictionary<string, string> _tablesWithDates = [];
private bool _tablesWithDatesInitialized;

private async Task<Dictionary<string, string>> GetTablesWithDatesAsync()
{
if (_tablesWithDatesInitialized) return _tablesWithDates;

using var connection = dbContext.Database.GetDbConnection();
await connection.OpenAsync();

var tableNames = new List<string>();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table'";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var name = reader.GetString(0);
if (!_tablesToIgnore.Contains(name)) tableNames.Add(name);
}
}

foreach (var tableName in tableNames)
{
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({tableName})";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var columnName = reader.GetString(1);
var columnType = reader.GetString(2);
if (IsDateColumn(columnName, columnType))
{
_tablesWithDates[tableName] = columnName;
logger.LogDebug("Found date column {ColumnName} in table {TableName}", columnName, tableName);
break;
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to analyze columns for table {TableName}", tableName);
}
}

_tablesWithDatesInitialized = true;
logger.LogInformation("Discovered {Count} tables with date columns: {Tables}",
_tablesWithDates.Count, string.Join(", ", _tablesWithDates.Select(kvp => $"{kvp.Key}({kvp.Value})")));

return _tablesWithDates;
}

private static bool IsDateColumn(string columnName, string columnType) =>
columnName.ToLower() switch
{
var name when name == "date" || name.EndsWith("date") || name.StartsWith("date") || name.Contains("timestamp") => true,
_ => columnType.ToLower() switch
{
var type when type.Contains("date") || type.Contains("datetime") || type.Contains("timestamp") => true,
_ => false
}
};
private readonly Lazy<Task<Dictionary<string, string>>> _tablesWithDatesCache = new(() => LoadTablesWithDatesAsync(dbContext, logger));

public override async Task<GetTableNamesResponse> GetTableNames(GetTableNamesRequest request, ServerCallContext context)
{
Expand Down Expand Up @@ -144,7 +81,7 @@
try
{
using var command = connection.CreateCommand();
command.CommandText = $"SELECT MAX({dateColumn}) FROM {tableName}";

Check warning on line 84 in PortfolioViewer/PortfolioViewer.ApiService/Services/SyncGrpcService.cs

View workflow job for this annotation

GitHub Actions / build-test-analyze

Make sure using a dynamically formatted SQL query is safe here. (https://rules.sonarsource.com/csharp/RSPEC-2077)
var result = await command.ExecuteScalarAsync(context.CancellationToken);
if (result is not null and not DBNull)
{
Expand All @@ -171,6 +108,71 @@
}
}

private async Task<Dictionary<string, string>> GetTablesWithDatesAsync() =>
await _tablesWithDatesCache.Value;

private static async Task<Dictionary<string, string>> LoadTablesWithDatesAsync(DatabaseContext dbContext, ILogger<SyncGrpcService> logger)
{
var tablesWithDates = new Dictionary<string, string>();
var tablesToIgnore = new[] { "sqlite_sequence", "__EFMigrationsHistory", "__EFMigrationsLock" };

using var connection = dbContext.Database.GetDbConnection();
await connection.OpenAsync();

var tableNames = new List<string>();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table'";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var name = reader.GetString(0);
if (!tablesToIgnore.Contains(name)) tableNames.Add(name);
}
}

foreach (var tableName in tableNames)
{
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({tableName})";

Check warning on line 139 in PortfolioViewer/PortfolioViewer.ApiService/Services/SyncGrpcService.cs

View workflow job for this annotation

GitHub Actions / build-test-analyze

Make sure using a dynamically formatted SQL query is safe here. (https://rules.sonarsource.com/csharp/RSPEC-2077)
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var columnName = reader.GetString(1);
var columnType = reader.GetString(2);
if (IsDateColumn(columnName, columnType))
{
tablesWithDates[tableName] = columnName;
logger.LogDebug("Found date column {ColumnName} in table {TableName}", columnName, tableName);
break;
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to analyze columns for table {TableName}", tableName);
}
}

logger.LogInformation("Discovered {Count} tables with date columns: {Tables}",
tablesWithDates.Count, string.Join(", ", tablesWithDates.Select(kvp => $"{kvp.Key}({kvp.Value})")));

return tablesWithDates;
}

private static bool IsDateColumn(string columnName, string columnType) =>
columnName.ToLower() switch
{
var name when name == "date" || name.EndsWith("date") || name.StartsWith("date") || name.Contains("timestamp") => true,
_ => columnType.ToLower() switch
{
var type when type.Contains("date") || type.Contains("datetime") || type.Contains("timestamp") => true,
_ => false
}
};

private async Task GetEntityDataInternal(string entity, int page, int pageSize, string? sinceDate, IServerStreamWriter<GetEntityDataResponse> responseStream)
{
if (page <= 0 || pageSize <= 0)
Expand Down Expand Up @@ -231,7 +233,7 @@
using var connection = dbContext.Database.GetDbConnection();
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = $"SELECT * FROM {entity} WHERE {dateColumn} >= @sinceDate ORDER BY {dateColumn} LIMIT @pageSize OFFSET @offset";

Check warning on line 236 in PortfolioViewer/PortfolioViewer.ApiService/Services/SyncGrpcService.cs

View workflow job for this annotation

GitHub Actions / build-test-analyze

Make sure using a dynamically formatted SQL query is safe here. (https://rules.sonarsource.com/csharp/RSPEC-2077)

var sinceDateParam = command.CreateParameter();
sinceDateParam.ParameterName = "@sinceDate";
Expand Down
Loading