using System.Data.Common; using Microsoft.EntityFrameworkCore; namespace MeterVault.Integration.Tests; [Collection("Timescale")] public sealed class SchemaTests(TimescaleFixture fx) { [Fact] public async Task Migrations_are_applied() { await using var ctx = fx.CreateContext(); var applied = (await ctx.Database.GetAppliedMigrationsAsync()).ToList(); Assert.Contains(applied, m => m.EndsWith("InitialSchema", StringComparison.Ordinal)); Assert.Contains(applied, m => m.EndsWith("TimescaleHypertables", StringComparison.Ordinal)); Assert.Contains(applied, m => m.EndsWith("AnalysisRollups", StringComparison.Ordinal)); } [Fact] public async Task The_continuous_aggregates_and_their_refresh_jobs_are_gone() { // D-17: nothing read them, they were Berlin-only and stale; the analysis tables replace them. await using var ctx = fx.CreateContext(); var conn = ctx.Database.GetDbConnection(); await conn.OpenAsync(); Assert.Empty(await QueryStringsAsync(conn, "SELECT view_name::text FROM timescaledb_information.continuous_aggregates;")); Assert.Empty(await QueryStringsAsync(conn, "SELECT proc_name::text FROM timescaledb_information.jobs WHERE proc_name = 'policy_refresh_continuous_aggregate';")); Assert.Empty(await QueryStringsAsync(conn, "SELECT relname::text FROM pg_class WHERE relname IN ('consumption_daily', 'consumption_monthly', 'consumption_yearly');")); } [Fact] public async Task The_analysis_tables_are_plain_tables_that_go_with_their_meter() { // D-12: plain tables (the hypertable list above stays reading + consumption), each tied to its meter by a // cascading foreign key, so deleting a meter — or wiping all of them — never trips over them. await using var ctx = fx.CreateContext(); var conn = ctx.Database.GetDbConnection(); await conn.OpenAsync(); var cascading = await QueryStringsAsync(conn, "SELECT tc.table_name::text FROM information_schema.table_constraints tc " + "JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name " + "JOIN information_schema.constraint_column_usage cu ON cu.constraint_name = tc.constraint_name " + "WHERE tc.constraint_type = 'FOREIGN KEY' AND rc.delete_rule = 'CASCADE' AND cu.table_name = 'meter' " + "AND tc.table_name IN ('consumption_rollup', 'consumption_rollup_month', 'meter_coverage', 'meter_rollup_state') " + "ORDER BY 1;"); Assert.Equal(["consumption_rollup", "consumption_rollup_month", "meter_coverage", "meter_rollup_state"], cascading); } [Fact] public async Task Reading_and_consumption_are_hypertables() { await using var ctx = fx.CreateContext(); var conn = ctx.Database.GetDbConnection(); await conn.OpenAsync(); var names = await QueryStringsAsync(conn, "SELECT hypertable_name FROM timescaledb_information.hypertables ORDER BY hypertable_name;"); Assert.Equal(["consumption", "reading"], names); } [Fact] public async Task Reading_has_a_compression_policy() { await using var ctx = fx.CreateContext(); var conn = ctx.Database.GetDbConnection(); await conn.OpenAsync(); // A compression/columnstore policy shows up as a scheduled job against the hypertable. var jobs = await QueryStringsAsync(conn, "SELECT hypertable_name FROM timescaledb_information.jobs " + "WHERE proc_name IN ('policy_compression', 'policy_columnstore') AND hypertable_name = 'reading';"); Assert.Contains("reading", jobs); } private static async Task> QueryStringsAsync(DbConnection conn, string sql) { await using var cmd = conn.CreateCommand(); cmd.CommandText = sql; var results = new List(); await using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(reader.GetString(0)); } return results; } }