diff --git a/src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs b/src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs index 3222859..3e69075 100644 --- a/src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs +++ b/src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs @@ -30,6 +30,77 @@ public static ContinuousAggregateBuilder IsContinuousAgg string? chunkInterval = null) where TEntity : class where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + /// + public static ContinuousAggregateBuilder IsContinuousAggregate( + this EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy = true, + string? chunkInterval = null) + where TEntity : class + where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + /// + public static ContinuousAggregateBuilder IsContinuousAggregate( + this EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy = true, + string? chunkInterval = null) + where TEntity : class + where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + /// + public static ContinuousAggregateBuilder IsContinuousAggregate( + this EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy = true, + string? chunkInterval = null) + where TEntity : class + where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + /// + public static ContinuousAggregateBuilder IsContinuousAggregate( + this EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy = true, + string? chunkInterval = null) + where TEntity : class + where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + /// + public static ContinuousAggregateBuilder IsContinuousAggregate( + this EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy = true, + string? chunkInterval = null) + where TEntity : class + where TSourceEntity : class + => IsContinuousAggregateCore(entityTypeBuilder, materializedViewName, timeBucketWidth, Box(propertyExpression), timeBucketGroupBy, chunkInterval); + + private static ContinuousAggregateBuilder IsContinuousAggregateCore( + EntityTypeBuilder entityTypeBuilder, + string materializedViewName, + string timeBucketWidth, + Expression> propertyExpression, + bool timeBucketGroupBy, + string? chunkInterval) + where TEntity : class + where TSourceEntity : class { // Configure the entity to map to a view instead of a table // This prevents EF Core from trying to create a table for the continuous aggregate @@ -52,5 +123,14 @@ public static ContinuousAggregateBuilder IsContinuousAgg return new ContinuousAggregateBuilder(entityTypeBuilder); } + + // Lifts a typed time-column expression to Expression> by inserting + // a Convert node, so the shared core method can extract the property name uniformly. + private static Expression> Box( + Expression> expression) + where TProperty : struct + => Expression.Lambda>( + Expression.Convert(expression.Body, typeof(object)), + expression.Parameters); } } diff --git a/src/Eftdb/Configuration/Hypertable/HypertableTypeBuilder.cs b/src/Eftdb/Configuration/Hypertable/HypertableTypeBuilder.cs index dc06f79..515c912 100644 --- a/src/Eftdb/Configuration/Hypertable/HypertableTypeBuilder.cs +++ b/src/Eftdb/Configuration/Hypertable/HypertableTypeBuilder.cs @@ -16,13 +16,48 @@ public static class HypertableTypeBuilder /// /// /// This is the essential first step to enable TimescaleDB features for an entity. - /// It corresponds to the `create_hypertable` function in PostgreSQL. + /// It corresponds to the create_hypertable function in PostgreSQL. /// /// The entity type being configured. /// The builder for the entity type. - /// A lambda expression representing the time column (e.g., `x => x.Timestamp`). + /// A lambda expression representing the time column (e.g., x => x.Timestamp). public static EntityTypeBuilder IsHypertable( this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + /// + public static EntityTypeBuilder IsHypertable( + this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + /// + public static EntityTypeBuilder IsHypertable( + this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + /// + public static EntityTypeBuilder IsHypertable( + this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + /// + public static EntityTypeBuilder IsHypertable( + this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + /// + public static EntityTypeBuilder IsHypertable( + this EntityTypeBuilder entityTypeBuilder, + Expression> timePropertyExpression) where TEntity : class + => IsHypertableCore(entityTypeBuilder, Box(timePropertyExpression)); + + private static EntityTypeBuilder IsHypertableCore( + EntityTypeBuilder entityTypeBuilder, Expression> timePropertyExpression) where TEntity : class { string propertyName = GetPropertyName(timePropertyExpression); @@ -33,6 +68,15 @@ public static EntityTypeBuilder IsHypertable( return entityTypeBuilder; } + // Lifts a typed time-column expression to Expression> by inserting a Convert + // node, so the shared core method can extract the property name uniformly via GetPropertyName. + private static Expression> Box( + Expression> expression) + where TProperty : struct + => Expression.Lambda>( + Expression.Convert(expression.Body, typeof(object)), + expression.Parameters); + /// /// Adds an additional partitioning dimension to the hypertable. /// diff --git a/tests/Eftdb.Tests/TypeBuilders/ContinuousAggregateBuilderTests.cs b/tests/Eftdb.Tests/TypeBuilders/ContinuousAggregateBuilderTests.cs index 34c27d1..dc53032 100644 --- a/tests/Eftdb.Tests/TypeBuilders/ContinuousAggregateBuilderTests.cs +++ b/tests/Eftdb.Tests/TypeBuilders/ContinuousAggregateBuilderTests.cs @@ -1580,4 +1580,289 @@ public void FluentAPI_Should_Support_Full_Method_Chaining() } #endregion + + #region IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn + + private class IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn_MetricEntity + { + public DateTimeOffset Timestamp { get; set; } + public double Value { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn_HourlyMetricAggregate + { + public DateTimeOffset TimeBucket { get; set; } + public double AvgValue { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn_Context : DbContext + { + public DbSet Metrics => Set(); + public DbSet HourlyMetrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("dto_metrics"); + entity.IsHypertable(x => x.Timestamp); + }); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.IsContinuousAggregate( + "dto_hourly_metrics", + "1 hour", + x => x.Timestamp) + .AddAggregateFunction(x => x.AvgValue, x => x.Value, EAggregateFunction.Avg); + }); + } + } + + [Fact] + public void IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn() + { + using IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn_Context context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IsContinuousAggregate_Should_Accept_DateTimeOffset_TimeColumn_HourlyMetricAggregate))!; + + Assert.Equal("dto_hourly_metrics", entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedViewName)?.Value); + Assert.Equal("Timestamp", entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketSourceColumn)?.Value); + } + + #endregion + + #region IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn + + private class IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn_MetricEntity + { + public DateOnly Day { get; set; } + public double Value { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn_DailyMetricAggregate + { + public DateOnly TimeBucket { get; set; } + public double AvgValue { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn_Context : DbContext + { + public DbSet Metrics => Set(); + public DbSet DailyMetrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("dateonly_metrics"); + entity.IsHypertable(x => x.Day); + }); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.IsContinuousAggregate( + "dateonly_daily_metrics", + "1 day", + x => x.Day) + .AddAggregateFunction(x => x.AvgValue, x => x.Value, EAggregateFunction.Avg); + }); + } + } + + [Fact] + public void IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn() + { + using IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn_Context context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IsContinuousAggregate_Should_Accept_DateOnly_TimeColumn_DailyMetricAggregate))!; + + Assert.Equal("dateonly_daily_metrics", entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedViewName)?.Value); + Assert.Equal("Day", entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketSourceColumn)?.Value); + } + + #endregion + + #region IsContinuousAggregate_Should_Accept_Long_TimeColumn + + private class IsContinuousAggregate_Should_Accept_Long_TimeColumn_MetricEntity + { + public long EpochMicros { get; set; } + public double Value { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Long_TimeColumn_BucketedMetricAggregate + { + public long TimeBucket { get; set; } + public double AvgValue { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Long_TimeColumn_Context : DbContext + { + public DbSet Metrics => Set(); + public DbSet Buckets => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("long_metrics"); + entity.IsHypertable(x => x.EpochMicros); + }); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.IsContinuousAggregate( + "long_bucketed_metrics", + "3600000000", + x => x.EpochMicros) + .AddAggregateFunction(x => x.AvgValue, x => x.Value, EAggregateFunction.Avg); + }); + } + } + + [Fact] + public void IsContinuousAggregate_Should_Accept_Long_TimeColumn() + { + using IsContinuousAggregate_Should_Accept_Long_TimeColumn_Context context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IsContinuousAggregate_Should_Accept_Long_TimeColumn_BucketedMetricAggregate))!; + + Assert.Equal("long_bucketed_metrics", entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedViewName)?.Value); + Assert.Equal("EpochMicros", entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketSourceColumn)?.Value); + } + + #endregion + + #region IsContinuousAggregate_Should_Accept_Int_TimeColumn + + private class IsContinuousAggregate_Should_Accept_Int_TimeColumn_MetricEntity + { + public int Ticks { get; set; } + public double Value { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Int_TimeColumn_BucketedMetricAggregate + { + public int TimeBucket { get; set; } + public double AvgValue { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Int_TimeColumn_Context : DbContext + { + public DbSet Metrics => Set(); + public DbSet Buckets => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("int_metrics"); + entity.IsHypertable(x => x.Ticks); + }); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.IsContinuousAggregate( + "int_bucketed_metrics", + "3600", + x => x.Ticks) + .AddAggregateFunction(x => x.AvgValue, x => x.Value, EAggregateFunction.Avg); + }); + } + } + + [Fact] + public void IsContinuousAggregate_Should_Accept_Int_TimeColumn() + { + using IsContinuousAggregate_Should_Accept_Int_TimeColumn_Context context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IsContinuousAggregate_Should_Accept_Int_TimeColumn_BucketedMetricAggregate))!; + + Assert.Equal("int_bucketed_metrics", entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedViewName)?.Value); + Assert.Equal("Ticks", entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketSourceColumn)?.Value); + } + + #endregion + + #region IsContinuousAggregate_Should_Accept_Short_TimeColumn + + private class IsContinuousAggregate_Should_Accept_Short_TimeColumn_MetricEntity + { + public short SlotIndex { get; set; } + public double Value { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Short_TimeColumn_BucketedMetricAggregate + { + public short TimeBucket { get; set; } + public double AvgValue { get; set; } + } + + private class IsContinuousAggregate_Should_Accept_Short_TimeColumn_Context : DbContext + { + public DbSet Metrics => Set(); + public DbSet Buckets => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("short_metrics"); + entity.IsHypertable(x => x.SlotIndex); + }); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.IsContinuousAggregate( + "short_bucketed_metrics", + "10", + x => x.SlotIndex) + .AddAggregateFunction(x => x.AvgValue, x => x.Value, EAggregateFunction.Avg); + }); + } + } + + [Fact] + public void IsContinuousAggregate_Should_Accept_Short_TimeColumn() + { + using IsContinuousAggregate_Should_Accept_Short_TimeColumn_Context context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IsContinuousAggregate_Should_Accept_Short_TimeColumn_BucketedMetricAggregate))!; + + Assert.Equal("short_bucketed_metrics", entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedViewName)?.Value); + Assert.Equal("SlotIndex", entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketSourceColumn)?.Value); + } + + #endregion } diff --git a/tests/Eftdb.Tests/TypeBuilders/HypertableTypeBuilderTests.cs b/tests/Eftdb.Tests/TypeBuilders/HypertableTypeBuilderTests.cs index 1ce8224..059f531 100644 --- a/tests/Eftdb.Tests/TypeBuilders/HypertableTypeBuilderTests.cs +++ b/tests/Eftdb.Tests/TypeBuilders/HypertableTypeBuilderTests.cs @@ -1012,4 +1012,204 @@ public void WithMigrateData_Should_Support_Method_Chaining() } #endregion + + #region IsHypertable_Should_Accept_DateTimeOffset_TimeColumn + + private class DateTimeOffsetTimeColumnEntity + { + public DateTimeOffset EventTime { get; set; } + public double Value { get; set; } + } + + private class DateTimeOffsetTimeColumnContext : DbContext + { + public DbSet Metrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("metrics_datetimeoffset_time"); + entity.IsHypertable(x => x.EventTime); + }); + } + } + + [Fact] + public void IsHypertable_Should_Accept_DateTimeOffset_TimeColumn() + { + using DateTimeOffsetTimeColumnContext context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(DateTimeOffsetTimeColumnEntity))!; + + Assert.Equal("EventTime", entityType.FindAnnotation(HypertableAnnotations.HypertableTimeColumn)?.Value); + Assert.Equal(true, entityType.FindAnnotation(HypertableAnnotations.IsHypertable)?.Value); + } + + #endregion + + #region IsHypertable_Should_Accept_DateOnly_TimeColumn + + private class DateOnlyTimeColumnEntity + { + public DateOnly EventDate { get; set; } + public double Value { get; set; } + } + + private class DateOnlyTimeColumnContext : DbContext + { + public DbSet Metrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("metrics_dateonly_time"); + entity.IsHypertable(x => x.EventDate); + }); + } + } + + [Fact] + public void IsHypertable_Should_Accept_DateOnly_TimeColumn() + { + using DateOnlyTimeColumnContext context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(DateOnlyTimeColumnEntity))!; + + Assert.Equal("EventDate", entityType.FindAnnotation(HypertableAnnotations.HypertableTimeColumn)?.Value); + Assert.Equal(true, entityType.FindAnnotation(HypertableAnnotations.IsHypertable)?.Value); + } + + #endregion + + #region IsHypertable_Should_Accept_Long_TimeColumn + + private class LongTimeColumnEntity + { + public long EventTimestamp { get; set; } + public double Value { get; set; } + } + + private class LongTimeColumnContext : DbContext + { + public DbSet Metrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("metrics_long_time"); + entity.IsHypertable(x => x.EventTimestamp); + }); + } + } + + [Fact] + public void IsHypertable_Should_Accept_Long_TimeColumn() + { + using LongTimeColumnContext context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(LongTimeColumnEntity))!; + + Assert.Equal("EventTimestamp", entityType.FindAnnotation(HypertableAnnotations.HypertableTimeColumn)?.Value); + Assert.Equal(true, entityType.FindAnnotation(HypertableAnnotations.IsHypertable)?.Value); + } + + #endregion + + #region IsHypertable_Should_Accept_Int_TimeColumn + + private class IntTimeColumnEntity + { + public int EventTimestamp { get; set; } + public double Value { get; set; } + } + + private class IntTimeColumnContext : DbContext + { + public DbSet Metrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("metrics_int_time"); + entity.IsHypertable(x => x.EventTimestamp); + }); + } + } + + [Fact] + public void IsHypertable_Should_Accept_Int_TimeColumn() + { + using IntTimeColumnContext context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(IntTimeColumnEntity))!; + + Assert.Equal("EventTimestamp", entityType.FindAnnotation(HypertableAnnotations.HypertableTimeColumn)?.Value); + Assert.Equal(true, entityType.FindAnnotation(HypertableAnnotations.IsHypertable)?.Value); + } + + #endregion + + #region IsHypertable_Should_Accept_Short_TimeColumn + + private class ShortTimeColumnEntity + { + public short EventTimestamp { get; set; } + public double Value { get; set; } + } + + private class ShortTimeColumnContext : DbContext + { + public DbSet Metrics => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql("Host=localhost;Database=test;Username=test;Password=test") + .UseTimescaleDb(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToTable("metrics_short_time"); + entity.IsHypertable(x => x.EventTimestamp); + }); + } + } + + [Fact] + public void IsHypertable_Should_Accept_Short_TimeColumn() + { + using ShortTimeColumnContext context = new(); + IModel model = GetModel(context); + IEntityType entityType = model.FindEntityType(typeof(ShortTimeColumnEntity))!; + + Assert.Equal("EventTimestamp", entityType.FindAnnotation(HypertableAnnotations.HypertableTimeColumn)?.Value); + Assert.Equal(true, entityType.FindAnnotation(HypertableAnnotations.IsHypertable)?.Value); + } + + #endregion }