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
130 changes: 67 additions & 63 deletions PortfolioViewer/PortfolioViewer.ApiService/Services/SyncGrpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,10 @@
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 Lazy<Task<Dictionary<string, string>>>? _tablesWithDatesCache;

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 Lazy<Task<Dictionary<string, string>>> TablesWithDatesCache =>
_tablesWithDatesCache ??= new(() => LoadTablesWithDatesAsync(dbContext, logger, _tablesToIgnore));

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

Check warning on line 87 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 +111,70 @@
}
}

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

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

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 141 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 +235,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 238 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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
</tr>
</thead>
<tbody>
@foreach (var div in dividends.OrderBy(d => d.ExDate))
@foreach (var div in dividends.OrderBy(d => d.PaymentDate))
{
<tr>
<td><strong>@div.Symbol</strong></td>
Expand All @@ -65,7 +65,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">—</span>
}
</td>
<td class="text-end">@div.Quantity.ToString("N2")</td>
Expand Down
Loading