refactor(database): update seed pipeline to use dapper (#277)

This commit is contained in:
Aaron Po
2026-09-05 00:37:14 -04:00
committed by GitHub
parent 78ad32e93a
commit d9411eb00e
43 changed files with 1023 additions and 735 deletions

View File

@@ -1,215 +1,259 @@
using System.Text.Json;
using Database.Seed.PipelineData;
using Dapper;
using Database.Seed.SourceDataModels;
using Microsoft.Data.Sqlite;
namespace Database.Seed.Sqlite;
public sealed record SeedData(
IReadOnlyList<BreweryRecord> Breweries,
IReadOnlyList<City> Cities,
IReadOnlyList<BreweryResult> Breweries,
IReadOnlyList<UserRecord> Users
);
public sealed class PipelineSeedDataReader
public sealed class SeedRepository : IDisposable
{
private readonly string _connectionString;
public PipelineSeedDataReader(string connectionString)
private readonly SqliteConnection connection;
public SeedRepository(string connectionString)
{
_connectionString = connectionString;
connection = new(connectionString);
}
public void Dispose()
{
connection.Dispose();
}
/// <summary>
/// Reads breweries and users in a single connection, loading the cities
/// lookup only once and sharing it between both reads.
/// </summary>
public async Task<SeedData> ReadSeedDataAsync(CancellationToken cancellationToken = default)
public async Task<SeedData> ReadSeedDataAsync(
CancellationToken cancellationToken = default
)
{
await using SqliteConnection connection = new(_connectionString);
await connection.OpenAsync(cancellationToken);
IReadOnlyDictionary<long, City> cities = await ReadCitiesAsync(
IReadOnlyDictionary<int, City> cities = await ReadCitiesAsync(
connection,
cancellationToken
);
IReadOnlyList<BreweryRecord> breweries = await ReadBreweryRecordsAsync(
IReadOnlyList<BreweryResult> breweries = await ReadBreweryRecordsAsync(
connection,
cities,
cancellationToken
);
IReadOnlyList<UserRecord> users = await ReadUserRecordsAsync(
connection,
cities,
cancellationToken
);
return new SeedData(breweries, users);
return new SeedData([.. cities.Values], breweries, users);
}
private static async Task<IReadOnlyList<BreweryRecord>> ReadBreweryRecordsAsync(
private sealed record BreweryRow(
int Id,
string NameEn,
string DescriptionEn,
string NameLocal,
string DescriptionLocal,
int AddressId,
int CityId,
double Longitude,
double Latitude,
string AddressLine1,
string PostalCode
);
private static async Task<IReadOnlyList<BreweryResult>> ReadBreweryRecordsAsync(
SqliteConnection connection,
IReadOnlyDictionary<long, City> cities,
IReadOnlyDictionary<int, City> cities,
CancellationToken cancellationToken
)
{
const string sql = """
SELECT b.name_en, b.description_en, b.name_local, b.description_local,
ba.city_id, ba.longitude, ba.latitude, ba.address_line1, ba.postal_code
FROM breweries b
JOIN brewery_addresses ba ON ba.brewery_id = b.id;
SELECT
b.id AS Id,
b.name_en AS NameEn,
b.description_en AS DescriptionEn,
b.name_local AS NameLocal,
b.description_local AS DescriptionLocal,
ba.id AS AddressId,
ba.city_id AS CityId,
ba.longitude AS Longitude,
ba.latitude AS Latitude,
ba.address_line1 AS AddressLine1,
ba.postal_code AS PostalCode
FROM
breweries b
INNER JOIN
brewery_addresses ba ON ba.brewery_id = b.id;
""";
await using SqliteCommand command = new(sql, connection);
await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
CommandDefinition command = new(sql, cancellationToken: cancellationToken);
IEnumerable<BreweryRow> rows = await connection.QueryAsync<BreweryRow>(command);
const int nameEnIndex = 0;
const int descriptionEnIndex = 1;
const int nameLocalIndex = 2;
const int descriptionLocalIndex = 3;
const int cityIdIndex = 4;
const int longitudeIndex = 5;
const int latitudeIndex = 6;
const int addressLine1Index = 7;
const int postalCodeIndex = 8;
List<BreweryRecord> records = [];
while (await reader.ReadAsync(cancellationToken))
records.Add(
new BreweryRecord
return [.. rows.Select(row => new BreweryResult
{
Id = row.Id,
NameEn = row.NameEn,
DescriptionEn = row.DescriptionEn,
NameLocal = row.NameLocal,
DescriptionLocal = row.DescriptionLocal,
Address = new BreweryAddress
{
Brewery = new BreweryResult
{
NameEn = reader.GetString(nameEnIndex),
DescriptionEn = reader.GetString(descriptionEnIndex),
NameLocal = reader.GetString(nameLocalIndex),
DescriptionLocal = reader.GetString(descriptionLocalIndex),
},
Address = new BreweryAddress
{
City = cities[reader.GetInt64(cityIdIndex)],
Longitude = reader.GetDouble(longitudeIndex),
Latitude = reader.GetDouble(latitudeIndex),
AddressLine1 = reader.GetString(addressLine1Index),
PostalCode = reader.GetString(postalCodeIndex),
},
}
);
return records;
Id = row.AddressId,
CityId = row.CityId,
BreweryId = row.Id,
Longitude = row.Longitude,
Latitude = row.Latitude,
AddressLine1 = row.AddressLine1,
PostalCode = row.PostalCode,
City = cities[row.CityId],
},
})];
}
private sealed record UserRow(
int Id,
string FirstName,
string LastName,
string Gender,
string Username,
string Bio,
double ActivityWeight,
string Email,
string DateOfBirth,
int AddressId,
int CityId,
double Longitude,
double Latitude
);
private static async Task<IReadOnlyList<UserRecord>> ReadUserRecordsAsync(
SqliteConnection connection,
IReadOnlyDictionary<long, City> cities,
IReadOnlyDictionary<int, City> cities,
CancellationToken cancellationToken
)
{
const string sql = """
SELECT u.first_name, u.last_name, u.gender, u.username, u.bio,
u.activity_weight, u.email, u.date_of_birth,
ua.city_id, ua.longitude, ua.latitude
FROM users u
JOIN user_addresses ua ON ua.user_id = u.id;
SELECT
u.id AS Id,
u.first_name AS FirstName,
u.last_name AS LastName,
u.gender AS Gender,
u.username AS Username,
u.bio AS Bio,
u.activity_weight AS ActivityWeight,
u.email AS Email,
u.date_of_birth AS DateOfBirth,
ua.id AS AddressId,
ua.city_id AS CityId,
ua.longitude AS Longitude,
ua.latitude AS Latitude
FROM
users u
INNER JOIN
user_addresses ua ON ua.user_id = u.id;
""";
await using SqliteCommand command = new(sql, connection);
await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
CommandDefinition command = new(sql, cancellationToken: cancellationToken);
IEnumerable<UserRow> rows = await connection.QueryAsync<UserRow>(command);
const int firstNameIndex = 0;
const int lastNameIndex = 1;
const int genderIndex = 2;
const int usernameIndex = 3;
const int bioIndex = 4;
const int activityWeightIndex = 5;
const int emailIndex = 6;
const int dateOfBirthIndex = 7;
const int cityIdIndex = 8;
const int longitudeIndex = 9;
const int latitudeIndex = 10;
List<UserRecord> records = [];
while (await reader.ReadAsync(cancellationToken))
records.Add(
new UserRecord
return [.. rows.Select(row => new UserRecord
{
Email = row.Email,
DateOfBirth = row.DateOfBirth,
Address = new UserAddress
{
User = new UserResult
{
FirstName = reader.GetString(firstNameIndex),
LastName = reader.GetString(lastNameIndex),
Gender = reader.GetString(genderIndex),
Username = reader.GetString(usernameIndex),
Bio = reader.GetString(bioIndex),
ActivityWeight = (float)reader.GetDouble(activityWeightIndex),
},
Email = reader.GetString(emailIndex),
DateOfBirth = reader.GetString(dateOfBirthIndex),
Address = new UserAddress
{
City = cities[reader.GetInt64(cityIdIndex)],
Longitude = reader.GetDouble(longitudeIndex),
Latitude = reader.GetDouble(latitudeIndex),
},
}
);
return records;
Id = row.AddressId,
CityId = row.CityId,
UserId = row.Id,
Longitude = row.Longitude,
Latitude = row.Latitude,
City = cities[row.CityId],
},
User = new UserResult
{
Id = row.Id,
FirstName = row.FirstName,
LastName = row.LastName,
Gender = row.Gender,
Username = row.Username,
Bio = row.Bio,
ActivityWeight = (float)row.ActivityWeight,
},
})];
}
private static async Task<IReadOnlyDictionary<long, City>> ReadCitiesAsync(
private sealed record CityRow(
int Id,
string CityName,
string StateProvince,
string Iso31662,
string Country,
string Iso31661,
double Longitude,
double Latitude,
string LocalLanguagesJson,
string PostalCodeCountryFormatRegex,
string PostalCodeCityRegexesJson
);
private static async Task<IReadOnlyDictionary<int, City>> ReadCitiesAsync(
SqliteConnection connection,
CancellationToken cancellationToken
)
{
const string sql = """
SELECT
id,
city,
state_province,
iso3166_2,
country,
iso3166_1,
local_languages_json,
postal_code_country_format_regex,
postal_code_city_regex_json
id AS Id,
city AS CityName,
state_province AS StateProvince,
iso3166_2 AS Iso31662,
country AS Country,
iso3166_1 AS Iso31661,
longitude AS Longitude,
latitude AS Latitude,
local_languages_json AS LocalLanguagesJson,
postal_code_country_format_regex AS PostalCodeCountryFormatRegex,
postal_code_city_regex_json AS PostalCodeCityRegexesJson
FROM cities;
""";
await using SqliteCommand command = new(sql, connection);
await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
CommandDefinition command = new(sql, cancellationToken: cancellationToken);
IEnumerable<CityRow> rows = await connection.QueryAsync<CityRow>(command);
Dictionary<long, City> cities = [];
const int idIndex = 0;
const int cityNameIndex = 1;
const int stateProvinceIndex = 2;
const int iso31662Index = 3;
const int countryIndex = 4;
const int iso31661Index = 5;
const int localLanguagesIndex = 6;
const int countryFormatRegexIndex = 7;
const int cityRegexesIndex = 8;
while (await reader.ReadAsync(cancellationToken))
cities[reader.GetInt64(idIndex)] = new City
return rows.ToDictionary(
row => row.Id,
row => new City
{
CityName = reader.GetString(cityNameIndex),
StateProvince = reader.GetString(stateProvinceIndex),
Iso31662 = reader.GetString(iso31662Index),
Country = reader.GetString(countryIndex),
Iso31661 = reader.GetString(iso31661Index),
LocalLanguages = DeserializeStringArray(reader.GetString(localLanguagesIndex)),
PostalCode = new PostalCodeSpec
{
CountryFormatRegex = reader.GetString(countryFormatRegexIndex),
CityRegexes = DeserializeStringArray(reader.GetString(cityRegexesIndex)),
},
};
return cities;
Id = row.Id,
CityName = row.CityName,
StateProvinceName = row.StateProvince,
ISO_3166_2 = row.Iso31662,
CountryName = row.Country,
ISO_3166_1 = row.Iso31661,
Longitude = row.Longitude,
Latitude = row.Latitude,
LocalLanguages = DeserializeStringArray(row.LocalLanguagesJson),
PostalCode = new PostalCodeSpec(
CountryFormatRegex: row.PostalCodeCountryFormatRegex,
CityRegexes: DeserializeStringArray(row.PostalCodeCityRegexesJson)
),
}
);
}
private static IReadOnlyList<string> DeserializeStringArray(string json)
{
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
}
}