Add sqlite load into Database.Seed

This commit is contained in:
Aaron Po
2026-07-20 00:55:21 -04:00
parent ec770aa724
commit 5fd60b33f7
15 changed files with 298 additions and 21 deletions

View File

@@ -16,7 +16,7 @@
<!--
remove this override once microsoft ships official fix to vulnerability
https://github.com/advisories/GHSA-2m69-gcr7-jv3q
https:
-->
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
</ItemGroup>
@@ -24,4 +24,10 @@
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Pipeline-generated seed data, copied to the output directory so it can be opened via
ConnectionStrings.SqliteConnectionString at runtime. -->
<None Include="SeedData\seed.sqlite" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -1,19 +1,9 @@
using Microsoft.Data.SqlClient;
namespace Database.Seed;
namespace Database.Seed.DatabaseHelpers;
public class ConnectionStrings
{
/// <summary>
/// Builds a SQL Server connection string from the <c>DB_SERVER</c>, <c>DB_NAME</c>,
/// <c>DB_USER</c>, <c>DB_PASSWORD</c>, and <c>DB_TRUST_SERVER_CERTIFICATE</c>
/// environment variables.
/// </summary>
/// <returns>A fully built SQL Server connection string.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>, or <c>DB_PASSWORD</c>
/// is not set.
/// </exception>
private static string GetSqlServerConnectionString()
{
string server =
@@ -48,7 +38,6 @@ public class ConnectionStrings
return builder.ConnectionString;
}
public static string SqlServerConnectionString => GetSqlServerConnectionString();
public static string SqliteConnectionString => "Data Source=seed.sqlite";
public static string SqliteConnectionString => "Data Source=SeedData/seed.sqlite";
}

View File

@@ -0,0 +1,8 @@
namespace Database.Seed.PipelineData;
public sealed record BreweryAddress
{
public required City City { get; init; }
public required string PostalCode { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Database.Seed.PipelineData;
public sealed record BreweryRecord
{
public required BreweryAddress Address { get; init; }
public required BreweryResult Brewery { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace Database.Seed.PipelineData;
public sealed record BreweryResult
{
public required string NameEn { get; init; }
public required string DescriptionEn { get; init; }
public required string NameLocal { get; init; }
public required string DescriptionLocal { get; init; }
}

View File

@@ -0,0 +1,18 @@
namespace Database.Seed.PipelineData;
public sealed record City
{
public required string CityName { get; init; }
public required string StateProvince { get; init; }
public required string Iso31662 { get; init; }
public required string Country { get; init; }
public required string Iso31661 { get; init; }
public IReadOnlyList<string> LocalLanguages { get; init; } = [];
public required PostalCodeSpec PostalCode { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Database.Seed.PipelineData;
public sealed record PostalCodeSpec
{
public required string CountryFormatRegex { get; init; }
public IReadOnlyList<string> CityRegexes { get; init; } = [];
public IReadOnlyList<string> Examples { get; init; } = [];
}

View File

@@ -0,0 +1,8 @@
namespace Database.Seed.PipelineData;
public sealed record UserAddress
{
public required City City { get; init; }
public required string PostalCode { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace Database.Seed.PipelineData;
public sealed record UserRecord
{
public required UserAddress Address { get; init; }
public required UserResult User { get; init; }
public required string Email { get; init; }
public required string DateOfBirth { get; init; }
}

View File

@@ -0,0 +1,16 @@
namespace Database.Seed.PipelineData;
public sealed record UserResult
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required string Gender { get; init; }
public required string Username { get; init; }
public required string Bio { get; init; }
public required float ActivityWeight { get; init; }
}

View File

@@ -0,0 +1,53 @@
using Database.Seed.DatabaseHelpers;
using Database.Seed.PipelineData;
using Database.Seed.Sqlite;
using Microsoft.Data.Sqlite;
namespace Database.Seed;
public class Program
{
public static int Main()
{
IReadOnlyList<BreweryRecord> breweries;
IReadOnlyList<UserRecord> users;
try
{
PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
breweries = reader.ReadBreweryRecords();
users = reader.ReadUserRecords();
}
catch (SqliteException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error opening database connection: {ex.Message}");
Console.ResetColor();
return 1;
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(
$"Loaded {breweries.Count} breweries."
);
Console.ResetColor();
for (int i = 0; i < breweries.Count; i++)
{
BreweryRecord brewery = breweries[i];
Console.WriteLine($"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Loaded {users.Count} users.");
Console.ResetColor();
for (int i = 0; i < users.Count; i++)
{
UserRecord user = users[i];
Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
}
return 0;
}
}

View File

@@ -0,0 +1,135 @@
using System.Text.Json;
using Database.Seed.PipelineData;
using Microsoft.Data.Sqlite;
namespace Database.Seed.Sqlite;
public sealed class PipelineSeedDataReader
{
private readonly string _connectionString;
public PipelineSeedDataReader(string connectionString)
{
_connectionString = connectionString;
}
public IReadOnlyList<BreweryRecord> ReadBreweryRecords()
{
using SqliteConnection connection = new(_connectionString);
connection.Open();
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
const string sql = """
SELECT b.name_en, b.description_en, b.name_local, b.description_local,
ba.city_id, ba.postal_code
FROM breweries b
JOIN brewery_addresses ba ON ba.brewery_id = b.id;
""";
using SqliteCommand command = new(sql, connection);
using SqliteDataReader reader = command.ExecuteReader();
List<BreweryRecord> records = [];
while (reader.Read())
records.Add(
new BreweryRecord
{
Brewery = new BreweryResult
{
NameEn = reader.GetString(0),
DescriptionEn = reader.GetString(1),
NameLocal = reader.GetString(2),
DescriptionLocal = reader.GetString(3),
},
Address = new BreweryAddress
{
City = cities[reader.GetInt64(4)],
PostalCode = reader.GetString(5),
},
}
);
return records;
}
public IReadOnlyList<UserRecord> ReadUserRecords()
{
using SqliteConnection connection = new(_connectionString);
connection.Open();
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
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.postal_code
FROM users u
JOIN user_addresses ua ON ua.user_id = u.id;
""";
using SqliteCommand command = new(sql, connection);
using SqliteDataReader reader = command.ExecuteReader();
List<UserRecord> records = [];
while (reader.Read())
records.Add(
new UserRecord
{
User = new UserResult
{
FirstName = reader.GetString(0),
LastName = reader.GetString(1),
Gender = reader.GetString(2),
Username = reader.GetString(3),
Bio = reader.GetString(4),
ActivityWeight = (float)reader.GetDouble(5),
},
Email = reader.GetString(6),
DateOfBirth = reader.GetString(7),
Address = new UserAddress
{
City = cities[reader.GetInt64(8)],
PostalCode = reader.GetString(9),
},
}
);
return records;
}
private static IReadOnlyDictionary<long, City> ReadCities(SqliteConnection connection)
{
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
FROM cities;
""";
using SqliteCommand command = new(sql, connection);
using SqliteDataReader reader = command.ExecuteReader();
Dictionary<long, City> cities = [];
while (reader.Read())
cities[reader.GetInt64(0)] = new City
{
CityName = reader.GetString(1),
StateProvince = reader.GetString(2),
Iso31662 = reader.GetString(3),
Country = reader.GetString(4),
Iso31661 = reader.GetString(5),
LocalLanguages = DeserializeStringArray(reader.GetString(6)),
PostalCode = new PostalCodeSpec
{
CountryFormatRegex = reader.GetString(7),
CityRegexes = DeserializeStringArray(reader.GetString(8)),
},
};
return cities;
}
private static IReadOnlyList<string> DeserializeStringArray(string json) =>
JsonSerializer.Deserialize<List<string>>(json) ?? [];
}