Compare commits
23 Commits
e57b3162db
...
main
Author | SHA1 | Date | |
---|---|---|---|
579517a1d4 | |||
d4ce3b2577 | |||
a1440baf3d | |||
ddd99ec703 | |||
965b693a19 | |||
ecbf2f07d6 | |||
68d91b8151 | |||
afd6f0298c | |||
fd9e4324dd | |||
f16e1e5e5d | |||
4c72a68481 | |||
0d30afd700 | |||
f5dda37285 | |||
87a98fefb1 | |||
23256245a0 | |||
226d819909 | |||
23b1f0f14e | |||
684a2c0630 | |||
673cda6408 | |||
39faab6483 | |||
5068d348af | |||
93e08a0378 | |||
322e6034bc |
@@ -8,6 +8,7 @@ namespace OpenCand.API.Config
|
||||
public const string DefaultPolicy = "DefaultPolicy";
|
||||
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
|
||||
public const string CpfRevealPolicy = "CpfRevealPolicy";
|
||||
public const string EstatisticaPolicy = "EstatisticaPolicy";
|
||||
|
||||
public static void ConfigureRateLimiting(this IServiceCollection services)
|
||||
{
|
||||
@@ -26,13 +27,12 @@ namespace OpenCand.API.Config
|
||||
// Default policy: 200 requests per minute with burst of 100
|
||||
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 200;
|
||||
options.PermitLimit = 400;
|
||||
options.Window = TimeSpan.FromMinutes(1);
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 100; // Burst capacity
|
||||
});
|
||||
|
||||
// Candidato Search policy: 300 requests per minute with burst of 200
|
||||
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 300;
|
||||
@@ -41,23 +41,27 @@ namespace OpenCand.API.Config
|
||||
options.QueueLimit = 200; // Burst capacity
|
||||
});
|
||||
|
||||
// CPF Reveal policy: 15 requests per minute without burst
|
||||
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 15;
|
||||
options.PermitLimit = 20;
|
||||
options.Window = TimeSpan.FromMinutes(1);
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 0; // No burst
|
||||
options.QueueLimit = 5; // Burst capacity
|
||||
});
|
||||
|
||||
options.OnRejected = async (context, token) =>
|
||||
{
|
||||
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("RateLimitingConfig");
|
||||
var clientIdentifier = GetClientIdentifier(context.HttpContext);
|
||||
logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier);
|
||||
|
||||
context.HttpContext.Response.StatusCode = 429;
|
||||
|
||||
var retryAfter = GetRetryAfter(context);
|
||||
if (retryAfter.HasValue)
|
||||
{
|
||||
context.HttpContext.Response.Headers.Add("Retry-After", retryAfter.Value.ToString());
|
||||
context.HttpContext.Response.Headers.Append("Retry-After", retryAfter.Value.ToString());
|
||||
}
|
||||
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
|
@@ -1,10 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using OpenCand.API.Config;
|
||||
|
||||
namespace OpenCand.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("v1/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||
public class BaseController : Controller
|
||||
{
|
||||
|
||||
|
@@ -1,14 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.VisualBasic;
|
||||
using OpenCand.API.Config;
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.API.Services;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Core.Utils;
|
||||
|
||||
namespace OpenCand.API.Controllers
|
||||
{
|
||||
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||
public class CandidatoController : BaseController
|
||||
{
|
||||
private readonly OpenCandService openCandService;
|
||||
@@ -22,18 +21,34 @@ namespace OpenCand.API.Controllers
|
||||
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
||||
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
||||
{
|
||||
if (string.IsNullOrEmpty(q) || q.Length == 1)
|
||||
if (string.IsNullOrEmpty(q) || q.Length < 3)
|
||||
{
|
||||
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
|
||||
}
|
||||
|
||||
return await openCandService.SearchCandidatosAsync(q);
|
||||
var result = await openCandService.SearchCandidatosAsync(q);
|
||||
|
||||
result.Candidatos.ForEach(c => c.Cpf = CpfMasking.MaskCpf(c.Cpf));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpGet("random")]
|
||||
public async Task<object> GetRandomCandidatoId()
|
||||
{
|
||||
return new
|
||||
{
|
||||
idCandidato = await openCandService.GetRandomCandidato()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<Candidato> GetCandidatoById([FromRoute] Guid id)
|
||||
{
|
||||
return await openCandService.GetCandidatoAsync(id);
|
||||
var result = await openCandService.GetCandidatoAsync(id);
|
||||
result.Cpf = CpfMasking.MaskCpf(result.Cpf);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/bens")]
|
||||
|
35
OpenCand.API/Controllers/EstatisticaController.cs
Normal file
35
OpenCand.API/Controllers/EstatisticaController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.API.Services;
|
||||
using static OpenCand.API.Model.GetValueSumRequest;
|
||||
|
||||
namespace OpenCand.API.Controllers
|
||||
{
|
||||
public class EstatisticaController : BaseController
|
||||
{
|
||||
private readonly EstatisticaService estatisticaService;
|
||||
|
||||
public EstatisticaController(EstatisticaService estatisticaService)
|
||||
{
|
||||
this.estatisticaService = estatisticaService;
|
||||
}
|
||||
|
||||
[HttpGet("configuration")]
|
||||
public async Task<ConfigurationModel> GetConfiguration()
|
||||
{
|
||||
return await estatisticaService.GetConfigurationModel();
|
||||
}
|
||||
|
||||
[HttpGet("enriquecimento")]
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos([FromQuery] GetValueSumRequestFilter requestFilter)
|
||||
{
|
||||
return await estatisticaService.GetMaioresEnriquecimentos(requestFilter);
|
||||
}
|
||||
|
||||
[HttpPost("values-sum")]
|
||||
public async Task<List<GetValueSumResponse>> GetValuesSum([FromBody] GetValueSumRequest getValueSumRequest)
|
||||
{
|
||||
return await estatisticaService.GetValueSum(getValueSumRequest);
|
||||
}
|
||||
}
|
||||
}
|
@@ -21,5 +21,18 @@ namespace OpenCand.API.Controllers
|
||||
{
|
||||
return await openCandService.GetOpenCandStatsAsync();
|
||||
}
|
||||
|
||||
[HttpGet("data-availability")]
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStats()
|
||||
{
|
||||
return await openCandService.GetDataAvailabilityStatsAsync();
|
||||
}
|
||||
|
||||
[HttpGet("tech")]
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStats()
|
||||
{
|
||||
return await openCandService.GetDatabaseTechStatsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
10
OpenCand.API/Model/ConfigurationModel.cs
Normal file
10
OpenCand.API/Model/ConfigurationModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace OpenCand.API.Model
|
||||
{
|
||||
public class ConfigurationModel
|
||||
{
|
||||
public List<string> Partidos { get; set; }
|
||||
public List<string> SiglasUF { get; set; }
|
||||
public List<int> Anos { get; set; }
|
||||
public List<string> Cargos { get; set; }
|
||||
}
|
||||
}
|
38
OpenCand.API/Model/EstatisticaModels.cs
Normal file
38
OpenCand.API/Model/EstatisticaModels.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace OpenCand.API.Model
|
||||
{
|
||||
public class MaioresEnriquecimento
|
||||
{
|
||||
public Guid IdCandidato { get; set; }
|
||||
public string Nome { get; set; }
|
||||
public float PatrimonioInicial { get; set; }
|
||||
public int AnoInicial { get; set; }
|
||||
public float PatrimonioFinal { get; set; }
|
||||
public int AnoFinal { get; set; }
|
||||
public float Enriquecimento { get; set; }
|
||||
}
|
||||
|
||||
public class GetValueSumRequest
|
||||
{
|
||||
public string Type { get; set; } // "bem", "despesa", "receita"
|
||||
public string GroupBy { get; set; } // "candidato", "partido", "uf", or "cargo"
|
||||
public GetValueSumRequestFilter? Filter { get; set; }
|
||||
public class GetValueSumRequestFilter
|
||||
{
|
||||
public string? Partido { get; set; } // Optional, can be null
|
||||
public string? Uf { get; set; } // Optional, can be null
|
||||
public int? Ano { get; set; } // Optional, can be null
|
||||
public string? Cargo { get; set; } // Optional, can be null
|
||||
}
|
||||
}
|
||||
|
||||
public class GetValueSumResponse
|
||||
{
|
||||
public Guid? IdCandidato { get; set; }
|
||||
public string? Sgpartido { get; set; }
|
||||
public string? SiglaUf { get; set; }
|
||||
public string? Cargo { get; set; }
|
||||
public string? Nome { get; set; }
|
||||
public int Ano { get; set; }
|
||||
public decimal Valor { get; set; }
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using OpenCand.API.Config;
|
||||
using OpenCand.API.Repository;
|
||||
@@ -35,7 +36,8 @@ namespace OpenCand.API
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
||||
RequestPath = "/assets/fotos"
|
||||
}); app.UseHttpsRedirection();
|
||||
});
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// Use rate limiting middleware
|
||||
app.UseRateLimiter();
|
||||
@@ -56,11 +58,25 @@ namespace OpenCand.API
|
||||
private static void SetupServices(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
|
||||
builder.Services.AddScoped<OpenCandRepository>();
|
||||
builder.Services.AddScoped<CandidatoRepository>();
|
||||
builder.Services.AddScoped<BemCandidatoRepository>();
|
||||
builder.Services.AddScoped<DespesaReceitaRepository>();
|
||||
|
||||
// Configure memory cache with size limit from appsettings
|
||||
var sizeLimitMB = builder.Configuration.GetValue<int>("CacheSettings:SizeLimitMB", 15);
|
||||
builder.Services.AddMemoryCache(options =>
|
||||
{
|
||||
options.SizeLimit = sizeLimitMB * 1024L * 1024L; // Convert MB to bytes
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(provider => new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new BemCandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new DespesaReceitaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new EstatisticaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
|
||||
builder.Services.AddScoped<OpenCandService>();
|
||||
builder.Services.AddScoped<EstatisticaService>();
|
||||
|
||||
// Add cache preload background service
|
||||
builder.Services.AddHostedService<CachePreloadService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace OpenCand.Repository
|
||||
{
|
||||
@@ -7,11 +9,153 @@ namespace OpenCand.Repository
|
||||
{
|
||||
protected string ConnectionString { get; private set; }
|
||||
protected NpgsqlConnection? Connection { get; private set; }
|
||||
protected readonly IMemoryCache? _cache;
|
||||
|
||||
public BaseRepository(IConfiguration configuration)
|
||||
// Default cache settings
|
||||
protected static readonly TimeSpan DefaultCacheExpiration = TimeSpan.MaxValue;
|
||||
protected static readonly CacheItemPriority DefaultCachePriority = CacheItemPriority.Normal;
|
||||
|
||||
public BaseRepository(IConfiguration configuration, IMemoryCache? cache = null)
|
||||
{
|
||||
ConnectionString = configuration["DatabaseSettings:ConnectionString"] ??
|
||||
throw new ArgumentNullException("Connection string not found in configuration");
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to get data from cache or execute a factory function if not cached
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of data to cache</typeparam>
|
||||
/// <param name="cacheKey">Unique cache key</param>
|
||||
/// <param name="factory">Function to execute if data is not in cache</param>
|
||||
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
|
||||
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
|
||||
/// <returns>Cached or freshly retrieved data</returns>
|
||||
protected async Task<T?> GetOrSetCacheAsync<T>(
|
||||
string cacheKey,
|
||||
Func<Task<T?>> factory,
|
||||
TimeSpan? expiration = null,
|
||||
CacheItemPriority? priority = null) where T : class
|
||||
{
|
||||
// If caching is not available, execute factory directly
|
||||
if (_cache == null)
|
||||
{
|
||||
return await factory();
|
||||
}
|
||||
|
||||
// Try to get cached data first
|
||||
if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null)
|
||||
{
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// If not in cache, execute factory function
|
||||
var result = await factory();
|
||||
|
||||
// Only cache non-null results
|
||||
if (result != null)
|
||||
{
|
||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
||||
Priority = priority ?? DefaultCachePriority,
|
||||
Size = EstimateSize(result)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to get data from cache or execute a synchronous factory function if not cached
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of data to cache</typeparam>
|
||||
/// <param name="cacheKey">Unique cache key</param>
|
||||
/// <param name="factory">Function to execute if data is not in cache</param>
|
||||
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
|
||||
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
|
||||
/// <returns>Cached or freshly retrieved data</returns>
|
||||
protected T? GetOrSetCache<T>(
|
||||
string cacheKey,
|
||||
Func<T?> factory,
|
||||
TimeSpan? expiration = null,
|
||||
CacheItemPriority? priority = null) where T : class
|
||||
{
|
||||
// If caching is not available, execute factory directly
|
||||
if (_cache == null)
|
||||
{
|
||||
return factory();
|
||||
}
|
||||
|
||||
// Try to get cached data first
|
||||
if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null)
|
||||
{
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// If not in cache, execute factory function
|
||||
var result = factory();
|
||||
|
||||
// Only cache non-null results
|
||||
if (result != null)
|
||||
{
|
||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = expiration ?? DefaultCacheExpiration,
|
||||
Priority = priority ?? DefaultCachePriority,
|
||||
Size = EstimateSize(result)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item from cache by key
|
||||
/// </summary>
|
||||
/// <param name="cacheKey">Cache key to remove</param>
|
||||
protected void ClearCache(string cacheKey)
|
||||
{
|
||||
_cache?.Remove(cacheKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an item exists in cache
|
||||
/// </summary>
|
||||
/// <param name="cacheKey">Cache key to check</param>
|
||||
/// <returns>True if item exists in cache, false otherwise</returns>
|
||||
protected bool IsCached(string cacheKey)
|
||||
{
|
||||
return _cache?.TryGetValue(cacheKey, out _) ?? false;
|
||||
} /// <summary>
|
||||
/// Generates a standardized cache key for entity-based caching
|
||||
/// </summary>
|
||||
/// <param name="entityName">Name of the entity (e.g., "Candidato", "Stats")</param>
|
||||
/// <param name="identifier">Unique identifier for the entity (optional)</param>
|
||||
/// <returns>Formatted cache key</returns>
|
||||
protected static string GenerateCacheKey(string entityName, object? identifier = null)
|
||||
{
|
||||
return identifier != null ? $"{entityName}_{identifier}" : entityName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the memory size of an object by serializing it to JSON
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the object</typeparam>
|
||||
/// <param name="obj">The object to estimate size for</param>
|
||||
/// <returns>Estimated size in bytes</returns>
|
||||
private static long EstimateSize<T>(T obj)
|
||||
{
|
||||
if (obj == null) return 0;
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
return Encoding.UTF8.GetByteCount(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 1024; // Default estimate if serialization fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
|
||||
@@ -6,7 +7,7 @@ namespace OpenCand.Repository
|
||||
{
|
||||
public class BemCandidatoRepository : BaseRepository
|
||||
{
|
||||
public BemCandidatoRepository(IConfiguration configuration) : base(configuration)
|
||||
public BemCandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
|
||||
@@ -6,48 +7,44 @@ namespace OpenCand.Repository
|
||||
{
|
||||
public class CandidatoRepository : BaseRepository
|
||||
{
|
||||
public CandidatoRepository(IConfiguration configuration) : base(configuration)
|
||||
public CandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<List<Candidato>> SearchCandidatosAsync(string query)
|
||||
public async Task<List<Candidato>?> SearchCandidatosAsync(string query)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
string cacheKey = GenerateCacheKey("Search", query);
|
||||
|
||||
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
return (await connection.QueryAsync<Candidato>(@"
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN lower(apelido) = lower(@query) THEN 0 -- apelido Exact match (case-insensitive)
|
||||
WHEN lower(apelido) LIKE lower(@query) || '%' THEN 1 -- apelido Starts with match (case-insensitive)
|
||||
WHEN lower(apelido) LIKE '%' || lower(@query) THEN 2 -- apelido Contains anywhere match (case-insensitive)
|
||||
WHEN lower(nome) = lower(@query) THEN 0 -- nome Exact match (case-insensitive)
|
||||
WHEN lower(nome) LIKE lower(@query) || '%' THEN 1 -- nome Starts with match (case-insensitive)
|
||||
WHEN lower(nome) LIKE '%' || lower(@query) THEN 2 -- nome Contains anywhere match (case-insensitive)
|
||||
WHEN cpf = @query THEN 0 -- cpf Exact match for CPF
|
||||
WHEN cpf LIKE @query || '%' THEN 1 -- cpf Starts with match for CPF
|
||||
WHEN cpf LIKE '%' || @query THEN 2 -- cpf Contains anywhere match for CPF
|
||||
ELSE 3
|
||||
END AS name_rank
|
||||
FROM candidato
|
||||
WHERE apelido ILIKE '%' || @query || '%' OR
|
||||
nome ILIKE '%' || @query || '%' OR
|
||||
cpf ILIKE '%' || @query || '%'
|
||||
ORDER BY name_rank,
|
||||
length(nome) ASC
|
||||
LIMIT 10;",
|
||||
new { query })).AsList();
|
||||
}
|
||||
SELECT c.*,
|
||||
GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim
|
||||
FROM candidato c
|
||||
WHERE c.apelido % @q
|
||||
OR c.nome % @q
|
||||
ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC
|
||||
LIMIT 10;
|
||||
", new { q = query })).AsList();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
string cacheKey = GenerateCacheKey("Candidato", idcandidato);
|
||||
|
||||
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
|
||||
SELECT * FROM candidato
|
||||
WHERE idcandidato = @idcandidato;",
|
||||
new { idcandidato });
|
||||
}
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
|
||||
SELECT * FROM candidato
|
||||
WHERE idcandidato = @idcandidato;",
|
||||
new { idcandidato });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
|
||||
@@ -93,5 +90,37 @@ namespace OpenCand.Repository
|
||||
return (await connection.QueryAsync<RedeSocial>(query, new { idcandidato })).AsList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CandidatoExt>?> GetCandidatoExtById(Guid idcandidato)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var query = @"
|
||||
SELECT * FROM candidato_ext
|
||||
WHERE idcandidato = @idcandidato";
|
||||
return (await connection.QueryAsync<CandidatoExt>(query, new { idcandidato })).AsList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task IncreaseCandidatoPopularity(Guid idcandidato)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
await connection.ExecuteAsync(@"
|
||||
UPDATE candidato
|
||||
SET popularidade = popularidade + 1
|
||||
WHERE idcandidato = @idcandidato;
|
||||
", new { idcandidato });
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidatoIdAsync()
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
return await connection.QueryFirstOrDefaultAsync<Guid?>(@"
|
||||
SELECT idcandidato
|
||||
FROM candidato
|
||||
TABLESAMPLE SYSTEM (0.01)
|
||||
LIMIT 1;
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
@@ -7,7 +8,7 @@ namespace OpenCand.Repository
|
||||
{
|
||||
public class DespesaReceitaRepository : BaseRepository
|
||||
{
|
||||
public DespesaReceitaRepository(IConfiguration configuration) : base(configuration)
|
||||
public DespesaReceitaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
}
|
||||
|
||||
|
124
OpenCand.API/Repository/EstatisticaRepository.cs
Normal file
124
OpenCand.API/Repository/EstatisticaRepository.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.Repository;
|
||||
using System.Text.Json;
|
||||
using static OpenCand.API.Model.GetValueSumRequest;
|
||||
|
||||
namespace OpenCand.API.Repository
|
||||
{
|
||||
public class EstatisticaRepository : BaseRepository
|
||||
{
|
||||
private readonly IConfiguration configuration;
|
||||
|
||||
public EstatisticaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||
{
|
||||
var joinBase = string.Empty;
|
||||
if (requestFilter == null) requestFilter = new GetValueSumRequestFilter();
|
||||
else
|
||||
{
|
||||
joinBase = " JOIN candidato_mapping cm ON ed.idcandidato = cm.idcandidato ";
|
||||
var whereConditions = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(requestFilter.Partido))
|
||||
{
|
||||
whereConditions.Add($"cm.sgpartido = '{requestFilter.Partido}'");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(requestFilter.Uf))
|
||||
{
|
||||
whereConditions.Add($"cm.siglauf = '{requestFilter.Uf.ToUpper()}'");
|
||||
}
|
||||
if (requestFilter.Ano != null)
|
||||
{
|
||||
whereConditions.Add($"cm.ano = '{requestFilter.Ano}'");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(requestFilter.Cargo))
|
||||
{
|
||||
whereConditions.Add($"cm.cargo = '{requestFilter.Cargo}'");
|
||||
}
|
||||
if (whereConditions.Count > 0)
|
||||
{
|
||||
joinBase += " WHERE " + string.Join(" AND ", whereConditions);
|
||||
}
|
||||
}
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(requestFilter);
|
||||
|
||||
string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento", requestJson);
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
return (await connection.QueryAsync<MaioresEnriquecimento>(@"
|
||||
WITH extremos_declaracao AS (
|
||||
SELECT
|
||||
idcandidato,
|
||||
MIN(ano) AS anoInicial,
|
||||
MAX(ano) AS anoFinal
|
||||
FROM mv_bem_candidato
|
||||
GROUP BY idcandidato
|
||||
HAVING COUNT(ano) >= 2
|
||||
)
|
||||
SELECT
|
||||
ed.idcandidato,
|
||||
c.nome,
|
||||
ed.anoInicial,
|
||||
pi.valor AS patrimonioInicial,
|
||||
ed.anoFinal,
|
||||
pf.valor AS patrimonioFinal,
|
||||
(pf.valor - pi.valor) AS enriquecimento
|
||||
FROM extremos_declaracao ed
|
||||
JOIN candidato c ON ed.idcandidato = c.idcandidato
|
||||
JOIN mv_bem_candidato pi ON ed.idcandidato = pi.idcandidato AND ed.anoInicial = pi.ano
|
||||
JOIN mv_bem_candidato pf ON ed.idcandidato = pf.idcandidato AND ed.anoFinal = pf.ano
|
||||
" + joinBase + @"
|
||||
ORDER BY
|
||||
enriquecimento DESC
|
||||
LIMIT 25;")
|
||||
).AsList();
|
||||
}
|
||||
});
|
||||
return result ?? new List<MaioresEnriquecimento>();
|
||||
}
|
||||
|
||||
public async Task<ConfigurationModel> GetConfiguration()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("GetConfigurationModel");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
var result = new ConfigurationModel();
|
||||
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
result.Partidos = (await connection.QueryAsync<string>(@"SELECT DISTINCT sigla FROM partido ORDER BY sigla ASC;")).AsList();
|
||||
result.SiglasUF = (await connection.QueryAsync<string>(@"SELECT DISTINCT siglauf FROM candidato_mapping ORDER BY siglauf ASC;")).AsList();
|
||||
result.Anos = (await connection.QueryAsync<int>(@"SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC;")).AsList();
|
||||
result.Cargos = (await connection.QueryAsync<string>(@"SELECT DISTINCT cargo FROM candidato_mapping ORDER BY cargo ASC;")).AsList();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return result ?? new ConfigurationModel();
|
||||
}
|
||||
|
||||
public async Task<List<GetValueSumResponse>> GetValueSum(string query, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
string cacheKey = GenerateCacheKey(query.GetHashCode().ToString());
|
||||
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
return (await connection.QueryAsync<GetValueSumResponse>(query, parameters)).AsList();
|
||||
}
|
||||
}) ?? new List<GetValueSumResponse>();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,29 +1,143 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Repository;
|
||||
|
||||
namespace OpenCand.API.Repository
|
||||
{
|
||||
{
|
||||
public class OpenCandRepository : BaseRepository
|
||||
{
|
||||
public OpenCandRepository(IConfiguration configuration) : base(configuration)
|
||||
private readonly IConfiguration configuration;
|
||||
|
||||
public OpenCandRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
string cacheKey = GenerateCacheKey("OpenCandStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
|
||||
SELECT
|
||||
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
|
||||
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
|
||||
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
|
||||
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
|
||||
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
|
||||
return stats ?? new OpenCandStats();
|
||||
}
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
|
||||
SELECT
|
||||
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
|
||||
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
|
||||
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
|
||||
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
|
||||
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
|
||||
|
||||
return stats ?? new OpenCandStats();
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new OpenCandStats();
|
||||
}
|
||||
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityAsync()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("DataAvailabilityStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var stats = new DataAvailabilityStats();
|
||||
|
||||
// Get years for each data type separately
|
||||
var candidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC");
|
||||
var bemCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM bem_candidato ORDER BY ano DESC");
|
||||
var despesaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM despesas_candidato ORDER BY ano DESC");
|
||||
var receitaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM receitas_candidato ORDER BY ano DESC");
|
||||
var redeSocialCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM rede_social ORDER BY ano DESC");
|
||||
|
||||
stats.Candidatos = candidatosYears.ToList();
|
||||
stats.BemCandidatos = bemCandidatosYears.ToList();
|
||||
stats.DespesaCandidatos = despesaCandidatosYears.ToList();
|
||||
stats.ReceitaCandidatos = receitaCandidatosYears.ToList();
|
||||
stats.RedeSocialCandidatos = redeSocialCandidatosYears.ToList();
|
||||
|
||||
// Get all folders from appsetting `FotosSettings__BasePath`
|
||||
string basePath = configuration["FotosSettings:Path"] ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(basePath))
|
||||
throw new InvalidOperationException("Base path for photos is not configured.");
|
||||
|
||||
var directories = Directory.GetDirectories(basePath);
|
||||
if (directories.Any())
|
||||
stats.FotosCandidatos = directories
|
||||
.Select(dir => dir.Split(Path.DirectorySeparatorChar).Last().Split("_")[1].Replace("cand", ""))
|
||||
.Select(ano => Convert.ToInt32(ano))
|
||||
.Distinct()
|
||||
.OrderByDescending(ano => ano)
|
||||
.ToList();
|
||||
|
||||
return stats;
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new DataAvailabilityStats();
|
||||
}
|
||||
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("DatabaseTechStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var stats = new DatabaseTechStats();
|
||||
|
||||
|
||||
// Get table stats using reltuples for entries
|
||||
var tableStats = await connection.QueryAsync<TableStats>(@"
|
||||
SELECT
|
||||
pt.schemaname||'.'||pt.tablename as Name,
|
||||
pg_total_relation_size(pt.schemaname||'.'||pt.tablename) as TotalSize,
|
||||
COALESCE(c.reltuples,0)::bigint as Entries
|
||||
FROM pg_tables pt
|
||||
JOIN pg_class c ON c.relname = pt.tablename AND c.relkind = 'r'
|
||||
WHERE pt.schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(pt.schemaname||'.'||pt.tablename) DESC;");
|
||||
|
||||
|
||||
var tableStatsList = tableStats.ToList();
|
||||
stats.Tables = tableStatsList;
|
||||
stats.TotalSize = tableStatsList.Sum(t => t.TotalSize);
|
||||
stats.TotalEntries = tableStatsList.Sum(t => t.Entries);
|
||||
|
||||
// Get materialized view stats using reltuples for entries
|
||||
var materializedViewStats = await connection.QueryAsync<TableStats>(@"
|
||||
SELECT
|
||||
pmv.schemaname||'.'||pmv.matviewname as Name,
|
||||
pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) as TotalSize,
|
||||
COALESCE(c.reltuples,0)::bigint as Entries
|
||||
FROM pg_matviews pmv
|
||||
JOIN pg_class c ON c.relname = pmv.matviewname AND c.relkind = 'm'
|
||||
WHERE pmv.schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) DESC;");
|
||||
|
||||
stats.MaterializedViews = materializedViewStats.ToList();
|
||||
|
||||
// Get index stats
|
||||
var indexStats = await connection.QueryFirstOrDefaultAsync<IndexStats>(@"
|
||||
SELECT
|
||||
COUNT(*) as Amount,
|
||||
SUM(pg_relation_size(indexrelid)) as Size
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'public';");
|
||||
|
||||
stats.Indexes = indexStats ?? new IndexStats();
|
||||
|
||||
return stats;
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new DatabaseTechStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
OpenCand.API/Services/CachePreloadService.cs
Normal file
98
OpenCand.API/Services/CachePreloadService.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCand.API.Model;
|
||||
|
||||
namespace OpenCand.API.Services
|
||||
{
|
||||
public class CachePreloadService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<CachePreloadService> logger;
|
||||
|
||||
public CachePreloadService(IServiceProvider serviceProvider, ILogger<CachePreloadService> logger)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Wait a bit for the application to fully start up
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
logger.LogInformation("Starting cache preload process...");
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var estatisticaService = scope.ServiceProvider.GetRequiredService<EstatisticaService>();
|
||||
var openCandService = scope.ServiceProvider.GetRequiredService<OpenCandService>();
|
||||
|
||||
// First, preload single-call endpoints
|
||||
await PreloadSingleEndpoints(estatisticaService, openCandService);
|
||||
|
||||
await PerformPreLoadEstatistica(estatisticaService);
|
||||
|
||||
logger.LogInformation("Cache preload process completed.");
|
||||
}
|
||||
|
||||
private async Task PerformPreLoadEstatistica(EstatisticaService estatisticaService)
|
||||
{
|
||||
await PerformPreLoad("GetConfigurationModel", estatisticaService.GetConfigurationModel);
|
||||
|
||||
var types = new[] { "bem", "despesa", "receita" };
|
||||
var groupByValues = new[] { "candidato", "partido", "uf", "cargo" };
|
||||
|
||||
logger.LogInformation($"Preloading cache with GetValueSum requests (3 types * 4 groupBy combinations)");
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
foreach (var groupBy in groupByValues)
|
||||
{
|
||||
var request = new GetValueSumRequest
|
||||
{
|
||||
Type = type,
|
||||
GroupBy = groupBy,
|
||||
Filter = null // No filters as requested
|
||||
};
|
||||
|
||||
logger.LogDebug($"Executing cache preload request: Type={type}, GroupBy={groupBy}");
|
||||
|
||||
await PerformPreLoad("GetValueSum", () => estatisticaService.GetValueSum(request));
|
||||
}
|
||||
}
|
||||
|
||||
await PerformPreLoad("GetMaioresEnriquecimentos", () => estatisticaService.GetMaioresEnriquecimentos(null));
|
||||
}
|
||||
|
||||
private async Task PreloadSingleEndpoints(EstatisticaService estatisticaService, OpenCandService openCandService)
|
||||
{
|
||||
logger.LogInformation("Preloading single-call endpoints...");
|
||||
|
||||
await PerformPreLoad("GetOpenCandStatsAsync", openCandService.GetOpenCandStatsAsync);
|
||||
await PerformPreLoad("GetDatabaseTechStatsAsync", openCandService.GetDatabaseTechStatsAsync);
|
||||
await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync);
|
||||
|
||||
logger.LogInformation("Single-call endpoints preload completed");
|
||||
}
|
||||
|
||||
private async Task PerformPreLoad(string name, Func<Task> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug($"Executing cache preload for {name}");
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
await action();
|
||||
var duration = DateTime.UtcNow - startTime;
|
||||
|
||||
logger.LogInformation($"Cache preload completed for {name}: Duration={duration.TotalMilliseconds}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Failed to perform preload action for {name}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.Delay(100); // Small delay to avoid overwhelming the database
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
OpenCand.API/Services/EstatisticaService.cs
Normal file
176
OpenCand.API/Services/EstatisticaService.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.API.Repository;
|
||||
using static OpenCand.API.Model.GetValueSumRequest;
|
||||
|
||||
namespace OpenCand.API.Services
|
||||
{
|
||||
public class EstatisticaService
|
||||
{
|
||||
private readonly EstatisticaRepository estatisticaRepository;
|
||||
private readonly ILogger<OpenCandService> logger;
|
||||
|
||||
public EstatisticaService(
|
||||
EstatisticaRepository estatisticaRepository,
|
||||
ILogger<OpenCandService> logger)
|
||||
{
|
||||
this.estatisticaRepository = estatisticaRepository;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ConfigurationModel> GetConfigurationModel()
|
||||
{
|
||||
logger.LogInformation("Getting configuration model");
|
||||
|
||||
return await estatisticaRepository.GetConfiguration();
|
||||
}
|
||||
|
||||
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
|
||||
{
|
||||
logger.LogInformation($"Getting maiores enriquecimentos. Filters: Partido={requestFilter?.Partido}, Uf={requestFilter?.Uf}, Ano={requestFilter?.Ano}, Cargo={requestFilter?.Cargo}");
|
||||
|
||||
return await estatisticaRepository.GetMaioresEnriquecimentos(requestFilter);
|
||||
}
|
||||
|
||||
public async Task<List<GetValueSumResponse>> GetValueSum(GetValueSumRequest request)
|
||||
{
|
||||
logger.LogInformation($"Getting value sum for {request.Type} grouped by {request.GroupBy}. Filters: Partido={request.Filter?.Partido}, Uf={request.Filter?.Uf}, Ano={request.Filter?.Ano}, Cargo={request.Filter?.Cargo}");
|
||||
// count exec time
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
ValidateRequest(request);
|
||||
|
||||
var sqlBuilder = new SqlQueryBuilder();
|
||||
var query = sqlBuilder.BuildQuery(request);
|
||||
var parameters = sqlBuilder.GetParameters();
|
||||
|
||||
var result = await estatisticaRepository.GetValueSum(query, parameters);
|
||||
|
||||
stopwatch.Stop();
|
||||
logger.LogInformation($"GetValueSum - Execution time: {stopwatch.ElapsedMilliseconds} ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateRequest(GetValueSumRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Type))
|
||||
throw new ArgumentException("Type is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.GroupBy))
|
||||
throw new ArgumentException("GroupBy is required.");
|
||||
|
||||
var validTypes = new[] { "bem", "despesa", "receita" };
|
||||
if (!validTypes.Contains(request.Type.ToLower()))
|
||||
throw new ArgumentException($"Invalid type specified. Valid values are: {string.Join(", ", validTypes)}");
|
||||
|
||||
var validGroupBy = new[] { "candidato", "partido", "uf", "cargo" };
|
||||
if (!validGroupBy.Contains(request.GroupBy.ToLower()))
|
||||
throw new ArgumentException($"Invalid groupBy specified. Valid values are: {string.Join(", ", validGroupBy)}");
|
||||
}
|
||||
|
||||
private class SqlQueryBuilder
|
||||
{
|
||||
private readonly Dictionary<string, object> _parameters = new();
|
||||
private int _paramCounter = 0;
|
||||
|
||||
public Dictionary<string, object> GetParameters() => _parameters;
|
||||
|
||||
public string BuildQuery(GetValueSumRequest request)
|
||||
{
|
||||
var selectClause = BuildSelectClause(request.GroupBy);
|
||||
var fromClause = BuildFromClause(request.Type);
|
||||
var joinClause = BuildJoinClause(request.GroupBy);
|
||||
var whereClause = BuildWhereClause(request.Filter);
|
||||
var groupByClause = BuildGroupByClause(request.GroupBy);
|
||||
var orderByClause = "ORDER BY SUM(src.valor) DESC";
|
||||
var limitClause = "LIMIT 10";
|
||||
|
||||
return $"{selectClause} {fromClause} {joinClause} {whereClause} {groupByClause} {orderByClause} {limitClause}";
|
||||
}
|
||||
|
||||
private string BuildSelectClause(string groupBy)
|
||||
{
|
||||
return groupBy.ToLower() switch
|
||||
{
|
||||
"candidato" => "SELECT src.idcandidato, cm.nome, src.ano, SUM(src.valor) as valor",
|
||||
"partido" => "SELECT cm.sgpartido, src.ano, SUM(src.valor) as valor",
|
||||
"uf" => "SELECT cm.siglauf, src.ano, SUM(src.valor) as valor",
|
||||
"cargo" => "SELECT cm.cargo, src.ano, SUM(src.valor) as valor",
|
||||
_ => throw new ArgumentException("Invalid group by specified.")
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildFromClause(string type)
|
||||
{
|
||||
return type.ToLower() switch
|
||||
{
|
||||
"bem" => "FROM mv_bem_candidato src",
|
||||
"despesa" => "FROM mv_despesas_candidato src",
|
||||
"receita" => "FROM mv_receitas_candidato src",
|
||||
_ => throw new ArgumentException("Invalid type specified.")
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildJoinClause(string groupBy)
|
||||
{
|
||||
return groupBy.ToLower() switch
|
||||
{
|
||||
"candidato" => "JOIN mv_candidato_mapping_analytics cm ON src.idcandidato = cm.idcandidato AND src.ano = cm.ano",
|
||||
"partido" or "uf" or "cargo" => "JOIN mv_candidato_mapping_analytics cm ON src.idcandidato = cm.idcandidato AND src.ano = cm.ano",
|
||||
_ => throw new ArgumentException("Invalid group by specified.")
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildWhereClause(GetValueSumRequest.GetValueSumRequestFilter? filter)
|
||||
{
|
||||
if (filter == null)
|
||||
return "";
|
||||
|
||||
var conditions = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Partido))
|
||||
{
|
||||
var paramName = $"partido{++_paramCounter}";
|
||||
conditions.Add($"cm.sgpartido = @{paramName}");
|
||||
_parameters[paramName] = filter.Partido;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Uf))
|
||||
{
|
||||
var paramName = $"uf{++_paramCounter}";
|
||||
conditions.Add($"cm.siglauf = @{paramName}");
|
||||
_parameters[paramName] = filter.Uf;
|
||||
}
|
||||
|
||||
if (filter.Ano.HasValue)
|
||||
{
|
||||
var paramName = $"ano{++_paramCounter}";
|
||||
conditions.Add($"cm.ano = @{paramName}");
|
||||
_parameters[paramName] = filter.Ano.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Cargo))
|
||||
{
|
||||
var paramName = $"cargo{++_paramCounter}";
|
||||
conditions.Add($"cm.cargo = @{paramName}");
|
||||
_parameters[paramName] = filter.Cargo;
|
||||
}
|
||||
|
||||
return conditions.Count > 0 ? $"WHERE {string.Join(" AND ", conditions)}" : "";
|
||||
}
|
||||
|
||||
private string BuildGroupByClause(string groupBy)
|
||||
{
|
||||
return groupBy.ToLower() switch
|
||||
{
|
||||
"candidato" => "GROUP BY src.idcandidato, cm.nome, src.ano",
|
||||
"partido" => "GROUP BY cm.sgpartido, src.ano",
|
||||
"uf" => "GROUP BY cm.siglauf, src.ano",
|
||||
"cargo" => "GROUP BY cm.cargo, src.ano",
|
||||
_ => throw new ArgumentException("Invalid group by specified.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -37,25 +37,62 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting OpenCand stats");
|
||||
|
||||
return await openCandRepository.GetOpenCandStatsAsync();
|
||||
}
|
||||
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting data availability stats");
|
||||
|
||||
var stats = await openCandRepository.GetDataAvailabilityAsync();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting database tech stats");
|
||||
|
||||
var stats = await openCandRepository.GetDatabaseTechStatsAsync();
|
||||
|
||||
stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList();
|
||||
stats.MaterializedViews = stats.MaterializedViews.OrderBy(mv => mv.Name).ToList();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
||||
{
|
||||
logger.LogInformation($"Searching candidatos with query: {query}");
|
||||
|
||||
return new CandidatoSearchResult()
|
||||
{
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query)
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query) ?? new List<Candidato>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidato()
|
||||
{
|
||||
logger.LogInformation("Getting random candidato");
|
||||
|
||||
return await candidatoRepository.GetRandomCandidatoIdAsync();
|
||||
}
|
||||
|
||||
public async Task<Candidato> GetCandidatoAsync(Guid idcandidato)
|
||||
{
|
||||
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
|
||||
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
|
||||
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
|
||||
|
||||
foreach (var eleicao in eleicoes)
|
||||
try
|
||||
{
|
||||
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
|
||||
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
@@ -66,11 +103,23 @@ namespace OpenCand.API.Services
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
||||
}
|
||||
if (candidatoExt == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoExt for ID {idcandidato} not found.");
|
||||
}
|
||||
|
||||
foreach (var eleicao in eleicoes)
|
||||
{
|
||||
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
|
||||
}
|
||||
|
||||
var lastEleicao = eleicoes.OrderByDescending(e => e.Ano).First();
|
||||
|
||||
result.FotoUrl = $"{fotoSettings.ApiBasePath}/foto_cand{lastEleicao.Ano}_{lastEleicao.SiglaUF}_div/F{lastEleicao.SiglaUF}{lastEleicao.SqCandidato}_div.jpg";
|
||||
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
|
||||
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
|
||||
|
||||
logger.LogDebug($"Found Candidato: {result.Nome}, ID: {result.IdCandidato}, CPF: {result.Cpf}, FotoUrl: {result.FotoUrl}");
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -83,6 +132,8 @@ namespace OpenCand.API.Services
|
||||
result = new List<BemCandidato>();
|
||||
}
|
||||
|
||||
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
|
||||
|
||||
return new BemCandidatoResult()
|
||||
{
|
||||
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
||||
@@ -91,12 +142,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
result = new List<RedeSocial>();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
return new RedeSocialResult()
|
||||
{
|
||||
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
||||
@@ -105,12 +160,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new CpfRevealResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}");
|
||||
|
||||
return new CpfRevealResult()
|
||||
{
|
||||
Cpf = result
|
||||
@@ -119,12 +178,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<DespesasResult> GetDespesasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting despesas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new DespesasResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} despesas for Candidato ID {idcandidato}");
|
||||
|
||||
return new DespesasResult()
|
||||
{
|
||||
Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
@@ -133,12 +196,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<ReceitaResult> GetReceitasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting receitas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new ReceitaResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} receitas for Candidato ID {idcandidato}");
|
||||
|
||||
return new ReceitaResult()
|
||||
{
|
||||
Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
|
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
},
|
||||
"DatabaseSettings": {
|
||||
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Include Error Detail=true;CommandTimeout=300"
|
||||
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20;Connection Lifetime=300;Command Timeout=30;Application Name=OpenCand.API;Include Error Detail=true"
|
||||
},
|
||||
"FotosSettings": {
|
||||
"Path": "./fotos_cand",
|
||||
"ApiBasePath": "http://localhost:5299/assets/fotos"
|
||||
},
|
||||
"CacheSettings": {
|
||||
"SizeLimitMB": 15
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd ./fotos_cand
|
||||
COUNT=0
|
||||
shopt -s nocaseglob
|
||||
|
||||
# Loop through all folders
|
||||
for dir in */; do
|
||||
# Change into the directory
|
||||
cd "$dir" || continue
|
||||
|
||||
# Loop over every “.jpeg” (or “.JPEG”):
|
||||
for f in *.jpeg; do
|
||||
# “${f%.[jJ][pP][eE][gG]}” strips off the .jpeg/.JPEG suffix
|
||||
base="${f%.[jJ][pP][eE][gG]}"
|
||||
newfile="${base}.jpg"
|
||||
|
||||
# If there’s already a .jpg with the same “base,” decide what to do:
|
||||
if [ -e "$newfile" ]; then
|
||||
echo "Skipping $f → $newfile (target exists)"
|
||||
# you could `rm "$f"` or move it to a backup folder here if you prefer
|
||||
else
|
||||
mv -v "$f" "$newfile"
|
||||
fi
|
||||
done
|
||||
|
||||
# Change back to the parent directory
|
||||
cd ..
|
||||
done
|
||||
|
||||
shopt -u nocaseglob
|
||||
|
||||
# Print a message indicating completion
|
||||
echo "Normalization complete. Processed $COUNT files."
|
@@ -20,14 +20,14 @@ namespace OpenCand.Core.Models
|
||||
|
||||
public string Sexo { get; set; }
|
||||
|
||||
public string EstadoCivil { get; set; }
|
||||
public string Localidade { get; set; }
|
||||
|
||||
public string Escolaridade { get; set; }
|
||||
|
||||
public string Ocupacao { get; set; }
|
||||
public int Ultimoano { get; set; }
|
||||
|
||||
public List<CandidatoMapping> Eleicoes { get; set; }
|
||||
|
||||
public List<CandidatoExt> CandidatoExt { get; set; }
|
||||
|
||||
// API ONLY
|
||||
public string FotoUrl { get; set; }
|
||||
}
|
||||
@@ -37,7 +37,6 @@ namespace OpenCand.Core.Models
|
||||
public Guid IdCandidato { get; set; }
|
||||
public string Cpf { get; set; }
|
||||
public string Nome { get; set; }
|
||||
public string Apelido { get; set; }
|
||||
public string SqCandidato { get; set; }
|
||||
public int Ano { get; set; }
|
||||
public string Turno { get; set; }
|
||||
@@ -52,6 +51,17 @@ namespace OpenCand.Core.Models
|
||||
public Partido? Partido { get; set; } // Nullable to allow for candidates without a party
|
||||
}
|
||||
|
||||
public class CandidatoExt
|
||||
{
|
||||
public Guid IdCandidato { get; set; }
|
||||
public int Ano { get; set; }
|
||||
public string Apelido { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string EstadoCivil { get; set; }
|
||||
public string Escolaridade { get; set; }
|
||||
public string Ocupacao { get; set; }
|
||||
}
|
||||
|
||||
public class RedeSocial
|
||||
{
|
||||
public Guid IdCandidato { get; set; }
|
||||
|
@@ -8,4 +8,37 @@
|
||||
public long TotalRedesSociais { get; set; }
|
||||
public long TotalEleicoes { get; set; }
|
||||
}
|
||||
|
||||
public class DataAvailabilityStats
|
||||
{
|
||||
public List<int> Candidatos { get; set; } = new List<int>();
|
||||
public List<int> BemCandidatos { get; set; } = new List<int>();
|
||||
public List<int> DespesaCandidatos { get; set; } = new List<int>();
|
||||
public List<int> ReceitaCandidatos { get; set; } = new List<int>();
|
||||
public List<int> RedeSocialCandidatos { get; set; } = new List<int>();
|
||||
|
||||
public List<int> FotosCandidatos { get; set; } = new List<int>();
|
||||
}
|
||||
|
||||
public class DatabaseTechStats
|
||||
{
|
||||
public List<TableStats> Tables { get; set; } = new List<TableStats>();
|
||||
public List<TableStats> MaterializedViews { get; set; } = new List<TableStats>();
|
||||
public IndexStats Indexes { get; set; } = new IndexStats();
|
||||
public long TotalSize { get; set; }
|
||||
public long TotalEntries { get; set; }
|
||||
}
|
||||
|
||||
public class TableStats
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long TotalSize { get; set; }
|
||||
public long Entries { get; set; }
|
||||
}
|
||||
|
||||
public class IndexStats
|
||||
{
|
||||
public int Amount { get; set; }
|
||||
public long Size { get; set; }
|
||||
}
|
||||
}
|
||||
|
21
OpenCand.Core/Utils/CpfMasking.cs
Normal file
21
OpenCand.Core/Utils/CpfMasking.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace OpenCand.Core.Utils
|
||||
{
|
||||
public static class CpfMasking
|
||||
{
|
||||
/// <summary>
|
||||
/// Masks a CPF number by replacing the middle 3 digits with '*'
|
||||
/// </summary>
|
||||
/// <param name="cpf">The CPF number to mask.</param>
|
||||
/// <returns>The masked CPF number.</returns>
|
||||
public static string MaskCpf(string cpf)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cpf) || cpf.Length != 11)
|
||||
{
|
||||
return cpf;
|
||||
}
|
||||
|
||||
// Mask the middle 3 digits
|
||||
return $"{cpf.Substring(0, 3)}***{cpf.Substring(6)}";
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,11 +2,14 @@
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool IsNullOrEmpty(this string value)
|
||||
public static bool IsNullOrEmpty(this string? value)
|
||||
{
|
||||
return string.IsNullOrEmpty(value) ||
|
||||
value == "#NE" ||
|
||||
value == "#NULO";
|
||||
value == "#NULO" ||
|
||||
value == "#NULO#" ||
|
||||
value == "NÃO DIVULGÁVEL" ||
|
||||
value == "-4";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,10 +36,10 @@ namespace OpenCand.Parser.Models
|
||||
public string Apelido { get; set; }
|
||||
|
||||
[Name("NR_CPF_CANDIDATO")]
|
||||
public string CPFCandidato { get; set; }
|
||||
public string? CPFCandidato { get; set; }
|
||||
|
||||
[Name("DS_EMAIL", "NM_EMAIL")]
|
||||
public string Email { get; set; }
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Name("DT_NASCIMENTO")]
|
||||
public string DataNascimento { get; set; }
|
||||
@@ -57,7 +57,7 @@ namespace OpenCand.Parser.Models
|
||||
public string GrauInstrucao { get; set; }
|
||||
|
||||
[Name("DS_SIT_TOT_TURNO")]
|
||||
public string SituacaoTurno { get; set; }
|
||||
public string? SituacaoTurno { get; set; }
|
||||
|
||||
[Name("NR_PARTIDO")]
|
||||
public int NumeroPartido { get; set; }
|
||||
|
@@ -2,6 +2,8 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenCand.Config;
|
||||
using OpenCand.ETL.Repository;
|
||||
using OpenCand.ETL.Services;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Parser.Services;
|
||||
|
||||
@@ -18,6 +20,10 @@ namespace OpenCand.Parser
|
||||
private readonly CsvParserService<DespesasCSV> despesaParserService;
|
||||
private readonly CsvParserService<ReceitasCSV> receitaParserService;
|
||||
|
||||
private readonly DespesaReceitaService despesaReceitaService;
|
||||
|
||||
private readonly ViewRepository viewRepository;
|
||||
|
||||
private readonly string BasePath;
|
||||
|
||||
public ParserManager(
|
||||
@@ -28,7 +34,9 @@ namespace OpenCand.Parser
|
||||
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
|
||||
CsvParserService<RedeSocialCSV> redeSocialParserService,
|
||||
CsvParserService<DespesasCSV> despesaParserService,
|
||||
CsvParserService<ReceitasCSV> receitaParserService)
|
||||
CsvParserService<ReceitasCSV> receitaParserService,
|
||||
DespesaReceitaService despesaReceitaService,
|
||||
ViewRepository viewRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.csvSettings = csvSettings.Value;
|
||||
@@ -38,6 +46,8 @@ namespace OpenCand.Parser
|
||||
this.redeSocialParserService = redeSocialParserService;
|
||||
this.despesaParserService = despesaParserService;
|
||||
this.receitaParserService = receitaParserService;
|
||||
this.despesaReceitaService = despesaReceitaService;
|
||||
this.viewRepository = viewRepository;
|
||||
|
||||
// Get the base path from either SampleFolder in csvSettings or the BasePath in configuration
|
||||
BasePath = configuration.GetValue<string>("BasePath") ?? string.Empty;
|
||||
@@ -58,13 +68,22 @@ namespace OpenCand.Parser
|
||||
var despesasDirectory = Path.Combine(BasePath, csvSettings.DespesaCandidatoFolder);
|
||||
var receitasDirectory = Path.Combine(BasePath, csvSettings.ReceitaCandidatoFolder);
|
||||
|
||||
//await ParseFolder(candidatosDirectory, candidatoParserService);
|
||||
//await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
|
||||
//await ParseFolder(redesSociaisDirectory, redeSocialParserService);
|
||||
await ParseFolder(candidatosDirectory, candidatoParserService);
|
||||
await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
|
||||
await ParseFolder(redesSociaisDirectory, redeSocialParserService);
|
||||
|
||||
await despesaReceitaService.DeleteDespesaAsync();
|
||||
await ParseFolder(despesasDirectory, despesaParserService);
|
||||
await despesaReceitaService.DeleteReceitaAsync();
|
||||
await ParseFolder(receitasDirectory, receitaParserService);
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Full data parsing completed!");
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Will refresh materialized views and re-run the analyzes.");
|
||||
|
||||
await viewRepository.RefreshMaterializedViews();
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Materialized views refreshed successfully!");
|
||||
}
|
||||
|
||||
private async Task ParseFolder<CsvObj>(string csvDirectory, CsvParserService<CsvObj> csvParserService)
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.ETL.Contracts;
|
||||
using OpenCand.ETL.Extensions;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Services;
|
||||
|
||||
@@ -22,62 +23,80 @@ namespace OpenCand.ETL.Parser.ParserServices
|
||||
|
||||
public async Task ParseObject(CandidatoCSV record)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3)
|
||||
if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty())
|
||||
{
|
||||
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
|
||||
}
|
||||
else
|
||||
{
|
||||
record.CPFCandidato = record.CPFCandidato.Trim();
|
||||
}
|
||||
|
||||
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
|
||||
string.IsNullOrEmpty(record.NomeCandidato) ||
|
||||
record.NomeCandidato == "#NULO")
|
||||
if (record.Apelido.IsNullOrEmpty())
|
||||
{
|
||||
record.Apelido = null;
|
||||
}
|
||||
|
||||
if (record.NomeCandidato.IsNullOrEmpty())
|
||||
{
|
||||
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
|
||||
return; // Skip candidates with invalid name
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(record.Apelido) ||
|
||||
record.Apelido.Contains("#NUL") ||
|
||||
record.Apelido.Contains("NULO#") ||
|
||||
record.Apelido.Contains("#NE"))
|
||||
if (record.Apelido.IsNullOrEmpty())
|
||||
{
|
||||
record.Apelido = null;
|
||||
}
|
||||
|
||||
if (record.SituacaoTurno.IsNullOrEmpty())
|
||||
{
|
||||
record.SituacaoTurno = null;
|
||||
}
|
||||
|
||||
var candidato = new Candidato
|
||||
{
|
||||
Cpf = record.CPFCandidato,
|
||||
SqCandidato = record.SequencialCandidato,
|
||||
Nome = record.NomeCandidato,
|
||||
Apelido = record.Apelido,
|
||||
Email = record.Email.Contains("@") ? record.Email : null,
|
||||
Sexo = record.Genero,
|
||||
EstadoCivil = record.EstadoCivil,
|
||||
Escolaridade = record.GrauInstrucao,
|
||||
Ocupacao = record.Ocupacao,
|
||||
Nome = record.NomeCandidato.Trim(),
|
||||
Apelido = record.Apelido?.Trim(),
|
||||
Sexo = record.Genero.Trim(),
|
||||
Localidade = record.NomeUE.Trim(),
|
||||
Ultimoano = record.AnoEleicao,
|
||||
Eleicoes = new List<CandidatoMapping>()
|
||||
{
|
||||
new CandidatoMapping
|
||||
{
|
||||
Cpf = record.CPFCandidato,
|
||||
Nome = record.NomeCandidato,
|
||||
Apelido = record.Apelido,
|
||||
SqCandidato = record.SequencialCandidato,
|
||||
Ano = record.AnoEleicao,
|
||||
Turno = record.Turno,
|
||||
TipoEleicao = record.TipoAbrangencia,
|
||||
NomeUE = record.NomeUE,
|
||||
SiglaUF = record.SiglaUF,
|
||||
Cargo = record.DescricaoCargo,
|
||||
NrCandidato = record.NumeroCandidato,
|
||||
Resultado = record.SituacaoTurno,
|
||||
Partido = new Partido
|
||||
{
|
||||
Sigla = record.SiglaPartido,
|
||||
Nome = record.NomePartido,
|
||||
Numero = record.NumeroPartido,
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
new CandidatoMapping
|
||||
{
|
||||
Cpf = record.CPFCandidato,
|
||||
Nome = record.NomeCandidato.Trim(),
|
||||
SqCandidato = record.SequencialCandidato.Trim(),
|
||||
Ano = record.AnoEleicao,
|
||||
Turno = record.Turno.Trim(),
|
||||
TipoEleicao = record.TipoAbrangencia.Trim(),
|
||||
NomeUE = record.NomeUE.Trim(),
|
||||
SiglaUF = record.SiglaUF.Trim(),
|
||||
Cargo = record.DescricaoCargo.Trim(),
|
||||
NrCandidato = record.NumeroCandidato.Trim(),
|
||||
Resultado = record.SituacaoTurno?.Trim() ?? "-",
|
||||
Partido = new Partido
|
||||
{
|
||||
Sigla = record.SiglaPartido.Trim(),
|
||||
Nome = record.NomePartido.Trim(),
|
||||
Numero = record.NumeroPartido,
|
||||
}
|
||||
}
|
||||
},
|
||||
CandidatoExt = new List<CandidatoExt>()
|
||||
{
|
||||
new CandidatoExt
|
||||
{
|
||||
Apelido = record.Apelido?.Trim(),
|
||||
EstadoCivil = record.EstadoCivil.Trim(),
|
||||
Escolaridade = record.GrauInstrucao.Trim(),
|
||||
Ocupacao = record.Ocupacao.Trim(),
|
||||
Ano = record.AnoEleicao,
|
||||
Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(record.DataNascimento) &&
|
||||
|
@@ -79,6 +79,7 @@ namespace OpenCand
|
||||
services.AddTransient<RedeSocialRepository>();
|
||||
services.AddTransient<PartidoRepository>();
|
||||
services.AddTransient<CsvFixerService>();
|
||||
services.AddTransient<ViewRepository>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -16,18 +16,17 @@ namespace OpenCand.Repository
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @email, @sexo, @estadocivil, @escolaridade, @ocupacao)
|
||||
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, sexo, ultimoano, localidade)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade)
|
||||
ON CONFLICT (idcandidato) DO UPDATE SET
|
||||
cpf = EXCLUDED.cpf,
|
||||
nome = EXCLUDED.nome,
|
||||
apelido = EXCLUDED.apelido,
|
||||
datanascimento = EXCLUDED.datanascimento,
|
||||
email = EXCLUDED.email,
|
||||
sexo = EXCLUDED.sexo,
|
||||
estadocivil = EXCLUDED.estadocivil,
|
||||
escolaridade = EXCLUDED.escolaridade,
|
||||
ocupacao = EXCLUDED.ocupacao,
|
||||
apelido = EXCLUDED.apelido;",
|
||||
localidade = EXCLUDED.localidade,
|
||||
ultimoano = EXCLUDED.ultimoano
|
||||
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidato.IdCandidato,
|
||||
@@ -35,11 +34,9 @@ namespace OpenCand.Repository
|
||||
nome = candidato.Nome,
|
||||
apelido = candidato.Apelido,
|
||||
datanascimento = candidato.DataNascimento,
|
||||
email = candidato.Email,
|
||||
sexo = candidato.Sexo,
|
||||
estadocivil = candidato.EstadoCivil,
|
||||
escolaridade = candidato.Escolaridade,
|
||||
ocupacao = candidato.Ocupacao
|
||||
localidade = candidato.Localidade,
|
||||
ultimoano = candidato.Ultimoano
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,15 +46,14 @@ namespace OpenCand.Repository
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato_mapping (idcandidato, cpf, nome, apelido, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
|
||||
INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
|
||||
VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
|
||||
ON CONFLICT DO NOTHING;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidatoMapping.IdCandidato,
|
||||
cpf = candidatoMapping.Cpf,
|
||||
nome = candidatoMapping.Nome,
|
||||
apelido = candidatoMapping.Apelido,
|
||||
sqcandidato = candidatoMapping.SqCandidato,
|
||||
ano = candidatoMapping.Ano,
|
||||
turno = candidatoMapping.Turno,
|
||||
@@ -72,6 +68,32 @@ namespace OpenCand.Repository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato_ext (idcandidato, ano, apelido, email, estadocivil, escolaridade, ocupacao)
|
||||
VALUES (@idcandidato, @ano, @apelido, @email, @estadocivil, @escolaridade, @ocupacao)
|
||||
ON CONFLICT (idcandidato, ano) DO UPDATE SET
|
||||
apelido = EXCLUDED.apelido,
|
||||
email = EXCLUDED.email,
|
||||
estadocivil = EXCLUDED.estadocivil,
|
||||
escolaridade = EXCLUDED.escolaridade,
|
||||
ocupacao = EXCLUDED.ocupacao;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidatoExt.IdCandidato,
|
||||
ano = candidatoExt.Ano,
|
||||
apelido = candidatoExt.Apelido,
|
||||
email = candidatoExt.Email,
|
||||
estadocivil = candidatoExt.EstadoCivil,
|
||||
escolaridade = candidatoExt.Escolaridade,
|
||||
ocupacao = candidatoExt.Ocupacao
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
|
@@ -135,5 +135,21 @@ namespace OpenCand.Repository
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteDespesaAsync()
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync("DELETE FROM despesas_candidato;");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteReceitaAsync()
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync("DELETE FROM receitas_candidato;");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
36
OpenCand.ETL/Repository/ViewRepository.cs
Normal file
36
OpenCand.ETL/Repository/ViewRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Repository;
|
||||
|
||||
namespace OpenCand.ETL.Repository
|
||||
{
|
||||
public class ViewRepository : BaseRepository
|
||||
{
|
||||
public ViewRepository(IConfiguration configuration) : base(configuration)
|
||||
{ }
|
||||
|
||||
public async Task RefreshMaterializedViews()
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
// Get all materialized view names
|
||||
var materializedViews = await connection.QueryAsync<string>(
|
||||
@"SELECT schemaname || '.' || matviewname as full_name
|
||||
FROM pg_matviews
|
||||
ORDER BY schemaname, matviewname");
|
||||
|
||||
foreach (var viewName in materializedViews)
|
||||
{
|
||||
// Refresh the materialized view
|
||||
await connection.ExecuteAsync($"REFRESH MATERIALIZED VIEW {viewName}");
|
||||
|
||||
// Analyze the materialized view to update statistics
|
||||
await connection.ExecuteAsync($"ANALYZE {viewName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,7 +17,7 @@ namespace OpenCand.Services
|
||||
|
||||
public async Task AddCandidatoAsync(Candidato candidato)
|
||||
{
|
||||
if (candidato == null)
|
||||
if (candidato == null || candidato.CandidatoExt == null || candidato.Eleicoes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null");
|
||||
}
|
||||
@@ -28,6 +28,7 @@ namespace OpenCand.Services
|
||||
}
|
||||
|
||||
var candidatoMapping = candidato.Eleicoes.First();
|
||||
var candidatoExt = candidato.CandidatoExt.First();
|
||||
|
||||
// Add partido data
|
||||
if (candidatoMapping.Partido != null)
|
||||
@@ -52,18 +53,18 @@ namespace OpenCand.Services
|
||||
candidato.IdCandidato = existingCandidato.IdCandidato;
|
||||
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
|
||||
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
|
||||
candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil);
|
||||
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
|
||||
candidato.Escolaridade = GetNonEmptyString(existingCandidato.Escolaridade, candidato.Escolaridade);
|
||||
candidato.Ocupacao = GetNonEmptyString(existingCandidato.Ocupacao, candidato.Ocupacao);
|
||||
candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo);
|
||||
|
||||
candidatoMapping.IdCandidato = candidato.IdCandidato;
|
||||
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
|
||||
|
||||
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||
|
||||
// Update the entries for the existing candidate
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,10 +80,12 @@ namespace OpenCand.Services
|
||||
if (existingMapping != null)
|
||||
{
|
||||
candidato.IdCandidato = existingMapping.IdCandidato;
|
||||
candidatoExt.IdCandidato = existingMapping.IdCandidato;
|
||||
|
||||
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
|
||||
candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido);
|
||||
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,8 +97,11 @@ namespace OpenCand.Services
|
||||
candidatoMapping.Cpf = candidato.Cpf;
|
||||
candidatoMapping.Nome = candidato.Nome;
|
||||
|
||||
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
}
|
||||
|
||||
public string GetNonEmptyString(string? value1, string? value2)
|
||||
|
@@ -50,5 +50,15 @@ namespace OpenCand.ETL.Services
|
||||
await despesaReceitaRepository.AddReceitaAsync(receita);
|
||||
}
|
||||
|
||||
public async Task DeleteDespesaAsync()
|
||||
{
|
||||
await despesaReceitaRepository.DeleteDespesaAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteReceitaAsync()
|
||||
{
|
||||
await despesaReceitaRepository.DeleteReceitaAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ namespace OpenCand.Services
|
||||
}
|
||||
|
||||
redeSocial.IdCandidato = candidato.IdCandidato;
|
||||
redeSocial.Rede = GetRedeSocialType(redeSocial.Link);
|
||||
redeSocial.Rede = GetRedeSocialType(redeSocial.Link.Trim());
|
||||
|
||||
await redeSocialRepository.AddRedeSocialAsync(redeSocial);
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"ParserSettings": {
|
||||
"DefaultThreads": 40,
|
||||
"CandidatoCSVThreads": 5,
|
||||
"CandidatoCSVThreads": 40,
|
||||
"DepesasCSVThreads": 50,
|
||||
"ReceitasCSVThreads": 50
|
||||
},
|
||||
|
159
db/db.sql
Normal file
159
db/db.sql
Normal file
@@ -0,0 +1,159 @@
|
||||
DROP TABLE IF EXISTS bem_candidato CASCADE;
|
||||
DROP TABLE IF EXISTS candidato_mapping CASCADE;
|
||||
DROP TABLE IF EXISTS candidato_ext CASCADE;
|
||||
DROP TABLE IF EXISTS rede_social CASCADE;
|
||||
DROP TABLE IF EXISTS candidato CASCADE;
|
||||
DROP TABLE IF EXISTS partido CASCADE;
|
||||
DROP TABLE IF EXISTS despesas_candidato CASCADE;
|
||||
DROP TABLE IF EXISTS receitas_candidato CASCADE;
|
||||
|
||||
CREATE TABLE candidato (
|
||||
idcandidato UUID NOT NULL PRIMARY KEY,
|
||||
cpf VARCHAR(11),
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
datanascimento TIMESTAMPTZ,
|
||||
sexo CHAR(15),
|
||||
apelido VARCHAR(255),
|
||||
localidade VARCHAR(100),
|
||||
ultimoano INT,
|
||||
popularidade BIGINT DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_candidato_nome ON candidato (nome);
|
||||
CREATE INDEX idx_candidato_apelido ON candidato (apelido);
|
||||
CREATE INDEX idx_candidato_datanascimento ON candidato (datanascimento);
|
||||
CREATE INDEX idx_candidato_nomenascimento ON candidato (nome, datanascimento);
|
||||
CREATE INDEX idx_candidato_cpf ON candidato (cpf);
|
||||
|
||||
-- Each candidato (idcandidato, cpf, nome) will be mapped to a (sqcandidato, ano, tipo_eleicao, sg_uf, cargo, resultado)
|
||||
CREATE TABLE candidato_mapping (
|
||||
idcandidato UUID NOT NULL,
|
||||
cpf VARCHAR(11),
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
tipoeleicao VARCHAR(50),
|
||||
siglauf VARCHAR(2),
|
||||
nomeue VARCHAR(100),
|
||||
cargo VARCHAR(50),
|
||||
sgpartido VARCHAR(50),
|
||||
nrcandidato VARCHAR(20),
|
||||
resultado VARCHAR(50),
|
||||
CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado),
|
||||
CONSTRAINT fk_candidato_mapping_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_candidato_mapping_idcandidato ON candidato_mapping (idcandidato);
|
||||
CREATE INDEX idx_candidato_mapping_cpf ON candidato_mapping (cpf);
|
||||
CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome);
|
||||
CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano);
|
||||
CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato);
|
||||
|
||||
CREATE TABLE candidato_ext (
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
apelido VARCHAR(255),
|
||||
email TEXT,
|
||||
estadocivil VARCHAR(50),
|
||||
escolaridade VARCHAR(50),
|
||||
ocupacao TEXT,
|
||||
CONSTRAINT pk_candidato_ext PRIMARY KEY (idcandidato, ano)
|
||||
);
|
||||
CREATE INDEX idx_candidato_ext_idcandidato ON candidato_ext (idcandidato);
|
||||
CREATE INDEX idx_candidato_ext_ano ON candidato_ext (ano);
|
||||
|
||||
---- Table for storing assets of candidates
|
||||
CREATE TABLE bem_candidato (
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
ordembem INT,
|
||||
tipobem VARCHAR(150),
|
||||
descricao VARCHAR(500),
|
||||
valor NUMERIC(20, 2),
|
||||
CONSTRAINT fk_bem_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
ALTER TABLE bem_candidato ADD CONSTRAINT pk_bem_candidato PRIMARY KEY (idcandidato, ano, ordembem);
|
||||
CREATE INDEX idx_bem_candidato_idcandidato ON bem_candidato (idcandidato);
|
||||
CREATE INDEX idx_bem_candidato_valor ON bem_candidato (valor);
|
||||
|
||||
---- Table for storing social media links of candidates
|
||||
CREATE TABLE rede_social (
|
||||
idcandidato UUID NOT NULL,
|
||||
rede VARCHAR(50) NOT NULL,
|
||||
siglauf VARCHAR(2),
|
||||
ano INT NOT NULL,
|
||||
link TEXT NOT NULL,
|
||||
CONSTRAINT pk_rede_social PRIMARY KEY (idcandidato, rede, siglauf, ano),
|
||||
CONSTRAINT fk_rede_social_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_rede_social_idcandidato ON rede_social (idcandidato);
|
||||
|
||||
---- Table for storing party information
|
||||
CREATE TABLE partido (
|
||||
sigla VARCHAR(50) NOT NULL PRIMARY KEY,
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
numero INT NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_partido_nome ON partido (nome);
|
||||
CREATE INDEX idx_partido_numero ON partido (numero);
|
||||
|
||||
---- Tables for storing despesas e receitas of candidacies
|
||||
CREATE TABLE despesas_candidato (
|
||||
iddespesa UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
sgpartido VARCHAR(50) NOT NULL,
|
||||
tipofornecedor VARCHAR(150),
|
||||
cnpjfornecedor VARCHAR(14),
|
||||
cpffornecedor VARCHAR(11),
|
||||
nomefornecedor VARCHAR(255),
|
||||
nomefornecedorrfb VARCHAR(255),
|
||||
municipiofornecedor VARCHAR(100),
|
||||
tipodocumento VARCHAR(50),
|
||||
datadespesa TIMESTAMPTZ,
|
||||
descricao TEXT,
|
||||
origemdespesa TEXT,
|
||||
valor NUMERIC(20, 2),
|
||||
CONSTRAINT pk_despesas_candidato PRIMARY KEY (iddespesa),
|
||||
CONSTRAINT fk_despesas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_despesas_candidato_idcandidato ON despesas_candidato (idcandidato);
|
||||
CREATE INDEX idx_despesas_candidato_ano ON despesas_candidato (ano);
|
||||
CREATE INDEX idx_despesas_candidato_sqcandidato ON despesas_candidato (sqcandidato);
|
||||
CREATE INDEX idx_despesas_candidato_sgpartido ON despesas_candidato (sgpartido);
|
||||
|
||||
CREATE TABLE receitas_candidato (
|
||||
idreceita UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
sgpartido VARCHAR(50) NOT NULL,
|
||||
fontereceita VARCHAR(150),
|
||||
origemreceita VARCHAR(250),
|
||||
naturezareceita VARCHAR(250),
|
||||
especiereceita VARCHAR(250),
|
||||
cnpjdoador VARCHAR(14),
|
||||
cpfdoador VARCHAR(11),
|
||||
nomedoador VARCHAR(255),
|
||||
nomedoadorrfb VARCHAR(255),
|
||||
municipiodoador VARCHAR(100),
|
||||
datareceita TIMESTAMPTZ,
|
||||
descricao TEXT,
|
||||
valor NUMERIC(20, 2),
|
||||
CONSTRAINT pk_receitas_candidato PRIMARY KEY (idreceita),
|
||||
CONSTRAINT fk_receitas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_receitas_candidato_idcandidato ON receitas_candidato (idcandidato);
|
||||
CREATE INDEX idx_receitas_candidato_ano ON receitas_candidato (ano);
|
||||
CREATE INDEX idx_receitas_candidato_sqcandidato ON receitas_candidato (sqcandidato);
|
||||
CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido);
|
||||
|
||||
|
||||
|
||||
-- Search function
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE INDEX idx_candidato_nome_trgm ON candidato USING GIN (nome gin_trgm_ops);
|
||||
CREATE INDEX idx_candidato_apelido_trgm ON candidato USING GIN (apelido gin_trgm_ops);
|
||||
CREATE INDEX idx_candidato_cpf_trgm ON candidato USING GIN (cpf gin_trgm_ops);
|
92
db/mv.sql
Normal file
92
db/mv.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- This script creates materialized views for bem_candidato, despesas_candidato, and receitas_candidato
|
||||
-- Drop existing materialized views if they exist
|
||||
DROP MATERIALIZED VIEW IF EXISTS mv_bem_candidato;
|
||||
DROP MATERIALIZED VIEW IF EXISTS mv_despesas_candidato;
|
||||
DROP MATERIALIZED VIEW IF EXISTS mv_receitas_candidato;
|
||||
|
||||
-- Creation stage
|
||||
---
|
||||
--- MV for bem_candidato
|
||||
CREATE MATERIALIZED VIEW mv_bem_candidato AS
|
||||
SELECT
|
||||
bem_candidato.ano,
|
||||
bem_candidato.idcandidato,
|
||||
candidato_mapping.siglauf as siglauf,
|
||||
candidato_mapping.sgpartido,
|
||||
cargo,
|
||||
SUM(valor) AS valor
|
||||
FROM
|
||||
bem_candidato
|
||||
JOIN candidato_mapping ON bem_candidato.idcandidato = candidato_mapping.idcandidato AND bem_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
|
||||
GROUP BY
|
||||
bem_candidato.ano,
|
||||
bem_candidato.idcandidato,
|
||||
candidato_mapping.sgpartido,
|
||||
siglauf,
|
||||
cargo;
|
||||
CREATE INDEX idx_mv_bem_candidato ON mv_bem_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
|
||||
|
||||
---
|
||||
--- MV for despesas_candidato
|
||||
CREATE MATERIALIZED VIEW mv_despesas_candidato AS
|
||||
SELECT
|
||||
despesas_candidato.ano,
|
||||
despesas_candidato.idcandidato,
|
||||
candidato_mapping.siglauf as siglauf,
|
||||
despesas_candidato.sgpartido,
|
||||
cargo,
|
||||
SUM(valor) AS valor
|
||||
FROM
|
||||
despesas_candidato
|
||||
JOIN candidato_mapping ON despesas_candidato.idcandidato = candidato_mapping.idcandidato AND despesas_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
|
||||
GROUP BY
|
||||
despesas_candidato.ano,
|
||||
despesas_candidato.idcandidato,
|
||||
despesas_candidato.sgpartido,
|
||||
siglauf,
|
||||
cargo;
|
||||
CREATE INDEX idx_mv_despesas_candidato ON mv_despesas_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
|
||||
|
||||
---
|
||||
--- MV for receitas_candidato
|
||||
CREATE MATERIALIZED VIEW mv_receitas_candidato AS
|
||||
SELECT
|
||||
receitas_candidato.ano,
|
||||
receitas_candidato.idcandidato,
|
||||
candidato_mapping.siglauf as siglauf,
|
||||
receitas_candidato.sgpartido,
|
||||
cargo,
|
||||
SUM(valor) AS valor
|
||||
FROM
|
||||
receitas_candidato
|
||||
JOIN candidato_mapping ON receitas_candidato.idcandidato = candidato_mapping.idcandidato AND receitas_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
|
||||
GROUP BY
|
||||
receitas_candidato.ano,
|
||||
receitas_candidato.idcandidato,
|
||||
receitas_candidato.sgpartido,
|
||||
siglauf,
|
||||
cargo;
|
||||
CREATE INDEX idx_mv_receitas_candidato ON mv_receitas_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
|
||||
|
||||
---
|
||||
--- View for candidate mapping summary
|
||||
CREATE MATERIALIZED VIEW mv_candidato_mapping_analytics AS
|
||||
SELECT DISTINCT cm.idcandidato, c.nome, cm.ano, cm.turno, cm.sgpartido, cm.siglauf, cm.cargo
|
||||
FROM candidato_mapping cm
|
||||
JOIN candidato c ON cm.idcandidato = c.idcandidato
|
||||
WHERE cm.turno = '1';
|
||||
|
||||
CREATE INDEX idx_mv_candidato_mapping_analytics ON mv_candidato_mapping_analytics (idcandidato, ano);
|
||||
|
||||
|
||||
-- Refresh the materialized views to ensure they are up-to-date
|
||||
REFRESH MATERIALIZED VIEW mv_bem_candidato;
|
||||
REFRESH MATERIALIZED VIEW mv_despesas_candidato;
|
||||
REFRESH MATERIALIZED VIEW mv_receitas_candidato;
|
||||
REFRESH MATERIALIZED VIEW mv_candidato_mapping_analytics;
|
||||
|
||||
-- Force re-analyze the materialized views to update statistics
|
||||
ANALYZE mv_bem_candidato;
|
||||
ANALYZE mv_despesas_candidato;
|
||||
ANALYZE mv_receitas_candidato;
|
||||
ANALYZE mv_candidato_mapping_analytics;
|
Reference in New Issue
Block a user