Files
satellite-provider/SatelliteProvider.DataAccess/Repositories/RouteRepository.cs
T
Oleksandr Bezdieniezhnykh 6099d1c86b
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-376] [AZ-378] [AZ-379] [AZ-380] Repo cleanup: dead code, logger discipline, ColumnList consts
Batch 23 of refactor 03-code-quality-refactoring (4 tasks, 5 SP):

- AZ-376 (C23): Delete unused FindExistingTileAsync from
  ITileRepository / TileRepository. No callers; method also took the
  obsolete `version` arg removed by C06/AZ-357.
- AZ-378 (C25): Repository _logger discipline.
  TileRepository.GetTilesByRegionAsync now emits LogWarning when the
  query exceeds SlowQueryThresholdMs (500 ms). RegionRepository and
  RouteRepository drop the unused ILogger<TRepo> field, parameter, and
  using; Program.cs DI registrations updated.
- AZ-379 (C26): Extract `private const string ColumnList` per repo
  (TileRepository, RegionRepository, RouteRepository); SELECTs use
  $@"SELECT {ColumnList} FROM ..." (C# 10+ const interpolation).
  INSERT/UPDATE/DELETE unchanged; route_points single-site SELECT left
  inline.
- AZ-380 (C27): Delete dead alias GeoUtils.CalculatePolygonDiagonalDistance.

Tests: +9 new (RepositoryRefactorTests x8, GeoUtilsRefactorTests x1)
covering each AC via reflection / file-content assertions; pattern
mirrors ToolingConfigurationTests (b22) and AcceptanceCriteriaRT2Tests
(b19). Unit suite 181 -> 190, all green. dotnet format clean.

Code review: PASS_WITH_WARNINGS (3 Low findings, all informational or
out-of-scope for this batch). See
_docs/03_implementation/reviews/batch_23_review.md.

Cumulative review counter 2/3; next K=3 review fires after batch 24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:57:49 +03:00

180 lines
7.5 KiB
C#

using Dapper;
using Npgsql;
using SatelliteProvider.DataAccess.Models;
namespace SatelliteProvider.DataAccess.Repositories;
public class RouteRepository : IRouteRepository
{
private const string ColumnList = @"id, name, description, region_size_meters as RegionSizeMeters,
zoom_level as ZoomLevel, total_distance_meters as TotalDistanceMeters,
total_points as TotalPoints, request_maps as RequestMaps,
maps_ready as MapsReady, create_tiles_zip as CreateTilesZip,
csv_file_path as CsvFilePath,
summary_file_path as SummaryFilePath, stitched_image_path as StitchedImagePath,
tiles_zip_path as TilesZipPath,
created_at as CreatedAt, updated_at as UpdatedAt";
private readonly string _connectionString;
public RouteRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<RouteEntity?> GetByIdAsync(Guid id)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = $@"
SELECT {ColumnList}
FROM routes
WHERE id = @Id";
return await connection.QuerySingleOrDefaultAsync<RouteEntity>(sql, new { Id = id });
}
public async Task<IEnumerable<RoutePointEntity>> GetRoutePointsAsync(Guid routeId)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT id, route_id as RouteId, sequence_number as SequenceNumber,
latitude, longitude, point_type as PointType, segment_index as SegmentIndex,
distance_from_previous as DistanceFromPrevious, created_at as CreatedAt
FROM route_points
WHERE route_id = @RouteId
ORDER BY sequence_number";
var points = (await connection.QueryAsync<RoutePointEntity>(sql, new { RouteId = routeId })).ToList();
return points;
}
public async Task<Guid> InsertRouteAsync(RouteEntity route)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
INSERT INTO routes (id, name, description, region_size_meters, zoom_level,
total_distance_meters, total_points, request_maps, maps_ready,
create_tiles_zip, csv_file_path, summary_file_path, stitched_image_path,
tiles_zip_path, created_at, updated_at)
VALUES (@Id, @Name, @Description, @RegionSizeMeters, @ZoomLevel,
@TotalDistanceMeters, @TotalPoints, @RequestMaps, @MapsReady,
@CreateTilesZip, @CsvFilePath, @SummaryFilePath, @StitchedImagePath,
@TilesZipPath, @CreatedAt, @UpdatedAt)
RETURNING id";
return await connection.ExecuteScalarAsync<Guid>(sql, route);
}
public async Task InsertRoutePointsAsync(IEnumerable<RoutePointEntity> points)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
INSERT INTO route_points (id, route_id, sequence_number, latitude, longitude,
point_type, segment_index, distance_from_previous, created_at)
VALUES (@Id, @RouteId, @SequenceNumber, @Latitude, @Longitude,
@PointType, @SegmentIndex, @DistanceFromPrevious, @CreatedAt)";
var pointsList = points.ToList();
await connection.ExecuteAsync(sql, pointsList);
}
public async Task<int> UpdateRouteAsync(RouteEntity route)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
UPDATE routes
SET name = @Name,
description = @Description,
region_size_meters = @RegionSizeMeters,
zoom_level = @ZoomLevel,
total_distance_meters = @TotalDistanceMeters,
total_points = @TotalPoints,
request_maps = @RequestMaps,
maps_ready = @MapsReady,
create_tiles_zip = @CreateTilesZip,
csv_file_path = @CsvFilePath,
summary_file_path = @SummaryFilePath,
stitched_image_path = @StitchedImagePath,
tiles_zip_path = @TilesZipPath,
updated_at = @UpdatedAt
WHERE id = @Id";
return await connection.ExecuteAsync(sql, route);
}
public async Task<int> DeleteRouteAsync(Guid id)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = "DELETE FROM routes WHERE id = @Id";
return await connection.ExecuteAsync(sql, new { Id = id });
}
public async Task LinkRouteToRegionAsync(Guid routeId, Guid regionId, bool isGeofence = false, int? geofencePolygonIndex = null)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
INSERT INTO route_regions (route_id, region_id, is_geofence, geofence_polygon_index, created_at)
VALUES (@RouteId, @RegionId, @IsGeofence, @GeofencePolygonIndex, @CreatedAt)
ON CONFLICT (route_id, region_id) DO NOTHING";
await connection.ExecuteAsync(sql, new { RouteId = routeId, RegionId = regionId, IsGeofence = isGeofence, GeofencePolygonIndex = geofencePolygonIndex, CreatedAt = DateTime.UtcNow });
}
public async Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT region_id
FROM route_regions
WHERE route_id = @RouteId AND (is_geofence = false OR is_geofence IS NULL)";
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
}
public async Task<IEnumerable<Guid>> GetGeofenceRegionIdsByRouteAsync(Guid routeId)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT region_id
FROM route_regions
WHERE route_id = @RouteId AND is_geofence = true";
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
}
public async Task<IEnumerable<RouteEntity>> GetRoutesWithPendingMapsAsync()
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = $@"
SELECT {ColumnList}
FROM routes
WHERE request_maps = true AND maps_ready = false";
return await connection.QueryAsync<RouteEntity>(sql);
}
public async Task<Dictionary<int, List<Guid>>> GetGeofenceRegionsByPolygonAsync(Guid routeId)
{
using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT region_id, geofence_polygon_index
FROM route_regions
WHERE route_id = @RouteId AND is_geofence = true AND geofence_polygon_index IS NOT NULL
ORDER BY geofence_polygon_index";
var results = await connection.QueryAsync<(Guid RegionId, int PolygonIndex)>(sql, new { RouteId = routeId });
var grouped = new Dictionary<int, List<Guid>>();
foreach (var (regionId, polygonIndex) in results)
{
if (!grouped.ContainsKey(polygonIndex))
{
grouped[polygonIndex] = new List<Guid>();
}
grouped[polygonIndex].Add(regionId);
}
return grouped;
}
}