Skip to content

Commit 8e32b8d

Browse files
fix: improve OrderBy consistency and add XML documentation
Fix scaffolding round-trip mismatch by omitting default NULLS clauses to NULLS FIRST). Change OrderByExtensions default from bool to bool? with null default to prevent phantom migrations between API styles. Replace var with explicit types and triple-slash inline comments with double-slash per coding standards.
1 parent 2e4d5ae commit 8e32b8d

8 files changed

Lines changed: 60 additions & 28 deletions

File tree

src/Eftdb.Design/Scaffolding/HypertableScaffoldingExtractor.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ List<Dimension> AdditionalDimensions
2929

3030
try
3131
{
32+
// TODO: Evtl. CompessionSettings und CompressionConfiguration combinen?
3233
Dictionary<(string, string), HypertableInfo> hypertables = [];
3334
Dictionary<(string, string), bool> compressionSettings = GetCompressionSettings(connection);
3435

@@ -210,10 +211,10 @@ FROM timescaledb_information.compression_settings
210211
bool isNullsFirst = reader.GetBoolean(6);
211212

212213
string direction = isAscending ? "ASC" : "DESC";
213-
string nulls = isNullsFirst ? "NULLS FIRST" : "NULLS LAST";
214+
bool isDefaultNulls = (isAscending && !isNullsFirst) || (!isAscending && isNullsFirst);
215+
string nulls = isDefaultNulls ? "" : (isNullsFirst ? " NULLS FIRST" : " NULLS LAST");
214216

215-
// Reconstruct the full string format: "colName DESC NULLS LAST"
216-
info.CompressionOrderBy.Add($"{columnName} {direction} {nulls}");
217+
info.CompressionOrderBy.Add($"{columnName} {direction}{nulls}");
217218
}
218219
}
219220
}

src/Eftdb/Abstractions/OrderBy.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Linq.Expressions;
2+
using System.Text;
23

4+
// TODO: Evtl. in .Configuration.Hypertable statt .Abstractions verschieben?
35
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions
46
{
57
/// <summary>
@@ -18,13 +20,21 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions
1820
/// </param>
1921
public class OrderBy(string columnName, bool? isAscending = null, bool? nullsFirst = null)
2022
{
23+
/// <summary>The name of the column to order by.</summary>
2124
public string ColumnName { get; } = columnName;
25+
26+
/// <summary>Ordering direction. True for ASC, false for DESC, null for database default.</summary>
2227
public bool? IsAscending { get; } = isAscending;
28+
29+
/// <summary>Null sorting behavior. True for NULLS FIRST, false for NULLS LAST, null for database default.</summary>
2330
public bool? NullsFirst { get; } = nullsFirst;
2431

32+
/// <summary>
33+
/// Converts this ordering specification to a SQL clause fragment.
34+
/// </summary>
2535
public string ToSql()
2636
{
27-
var sb = new System.Text.StringBuilder(ColumnName);
37+
StringBuilder sb = new(ColumnName);
2838

2939
// Only append direction if explicitly set
3040
if (IsAscending.HasValue)
@@ -47,6 +57,11 @@ public string ToSql()
4757
/// </summary>
4858
public static class OrderByBuilder
4959
{
60+
/// <summary>
61+
/// Starts building an OrderBy specification for the specified property.
62+
/// </summary>
63+
/// <typeparam name="TEntity">The entity type containing the property.</typeparam>
64+
/// <param name="expression">A lambda expression selecting the property to order by.</param>
5065
public static OrderByConfiguration<TEntity> For<TEntity>(Expression<Func<TEntity, object>> expression) => new(expression);
5166
}
5267

@@ -57,8 +72,16 @@ public class OrderByConfiguration<TEntity>(Expression<Func<TEntity, object>> exp
5772
{
5873
private readonly string _propertyName = GetPropertyName(expression);
5974

75+
/// <summary>Creates an OrderBy using the database default direction.</summary>
76+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
6077
public OrderBy Default(bool? nullsFirst = null) => new(_propertyName, null, nullsFirst);
78+
79+
/// <summary>Creates an ascending OrderBy specification.</summary>
80+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
6181
public OrderBy Ascending(bool? nullsFirst = null) => new(_propertyName, true, nullsFirst);
82+
83+
/// <summary>Creates a descending OrderBy specification.</summary>
84+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
6285
public OrderBy Descending(bool? nullsFirst = null) => new(_propertyName, false, nullsFirst);
6386

6487
// Helper to extract the string name from the expression
@@ -76,16 +99,24 @@ private static string GetPropertyName(Expression<Func<TEntity, object>> expressi
7699
/// <typeparam name="TEntity"></typeparam>
77100
public class OrderBySelector<TEntity>
78101
{
102+
/// <summary>Creates an OrderBy using the database default direction for the selected property.</summary>
103+
/// <param name="expression">A lambda expression selecting the property to order by.</param>
104+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
79105
public OrderBy By(Expression<Func<TEntity, object>> expression, bool? nullsFirst = null)
80106
=> new(GetPropertyName(expression), null, nullsFirst);
81107

108+
/// <summary>Creates an ascending OrderBy specification for the selected property.</summary>
109+
/// <param name="expression">A lambda expression selecting the property to order by.</param>
110+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
82111
public OrderBy ByAscending(Expression<Func<TEntity, object>> expression, bool? nullsFirst = null)
83112
=> new(GetPropertyName(expression), true, nullsFirst);
84113

114+
/// <summary>Creates a descending OrderBy specification for the selected property.</summary>
115+
/// <param name="expression">A lambda expression selecting the property to order by.</param>
116+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
85117
public OrderBy ByDescending(Expression<Func<TEntity, object>> expression, bool? nullsFirst = null)
86118
=> new(GetPropertyName(expression), false, nullsFirst);
87119

88-
// Internal helper to get property names from expressions
89120
private static string GetPropertyName(Expression<Func<TEntity, object>> expression)
90121
{
91122
if (expression.Body is MemberExpression m) return m.Member.Name;
@@ -103,8 +134,8 @@ public static class OrderByExtensions
103134
/// Creates an ascending OrderBy instance.
104135
/// </summary>
105136
/// <param name="columnName">The name of the column to order by.</param>
106-
/// <param name="nullsFirst">Whether nulls should appear first.</param>
107-
public static OrderBy Ascending(this string columnName, bool nullsFirst = false)
137+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
138+
public static OrderBy Ascending(this string columnName, bool? nullsFirst = null)
108139
{
109140
return new OrderBy(columnName, true, nullsFirst);
110141
}
@@ -113,8 +144,8 @@ public static OrderBy Ascending(this string columnName, bool nullsFirst = false)
113144
/// Creates a descending OrderBy instance.
114145
/// </summary>
115146
/// <param name="columnName">The name of the column to order by.</param>
116-
/// <param name="nullsFirst">Whether nulls should appear first.</param>
117-
public static OrderBy Descending(this string columnName, bool nullsFirst = false)
147+
/// <param name="nullsFirst">Optional null sorting behavior. Null uses database default.</param>
148+
public static OrderBy Descending(this string columnName, bool? nullsFirst = null)
118149
{
119150
return new OrderBy(columnName, false, nullsFirst);
120151
}

src/Eftdb/Configuration/Hypertable/HypertableConvention.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@ public void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilde
4444

4545
if (attribute.ChunkSkipColumns != null && attribute.ChunkSkipColumns.Length > 0)
4646
{
47-
/// Chunk skipping requires compression to be enabled
47+
// Chunk skipping requires compression to be enabled
4848
entityTypeBuilder.HasAnnotation(HypertableAnnotations.EnableCompression, true);
4949
entityTypeBuilder.HasAnnotation(HypertableAnnotations.ChunkSkipColumns, string.Join(",", attribute.ChunkSkipColumns));
5050
}
5151

5252
if (attribute.CompressionSegmentBy != null && attribute.CompressionSegmentBy.Length > 0)
5353
{
54-
/// SegmentBy requires compression to be enabled
54+
// SegmentBy requires compression to be enabled
5555
entityTypeBuilder.HasAnnotation(HypertableAnnotations.EnableCompression, true);
5656
entityTypeBuilder.HasAnnotation(HypertableAnnotations.CompressionSegmentBy, string.Join(", ", attribute.CompressionSegmentBy));
5757
}
5858

5959
if (attribute.CompressionOrderBy != null && attribute.CompressionOrderBy.Length > 0)
6060
{
61-
/// OrderBy requires compression to be enabled
61+
// OrderBy requires compression to be enabled
6262
entityTypeBuilder.HasAnnotation(HypertableAnnotations.EnableCompression, true);
6363
entityTypeBuilder.HasAnnotation(HypertableAnnotations.CompressionOrderBy, string.Join(", ", attribute.CompressionOrderBy));
6464
}

src/Eftdb/Configuration/Hypertable/HypertableTypeBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,10 @@ public static EntityTypeBuilder<TEntity> WithCompressionOrderBy<TEntity>(
178178
this EntityTypeBuilder<TEntity> entityTypeBuilder,
179179
Func<OrderBySelector<TEntity>, IEnumerable<OrderBy>> orderSelector) where TEntity : class
180180
{
181-
var selector = new OrderBySelector<TEntity>();
182-
var rules = orderSelector(selector);
181+
OrderBySelector<TEntity> selector = new();
182+
IEnumerable<OrderBy> rules = orderSelector(selector);
183183

184-
return entityTypeBuilder.WithCompressionOrderBy(rules.ToArray());
184+
return entityTypeBuilder.WithCompressionOrderBy([.. rules]);
185185
}
186186

187187
/// <summary>

src/Eftdb/Generators/HypertableOperationGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ private static string QuoteOrderByList(IEnumerable<string> orderByClauses)
322322
{
323323
return string.Join(", ", orderByClauses.Select(clause =>
324324
{
325-
var parts = clause.Split(' ', 2);
325+
string[] parts = clause.Split(' ', 2);
326326
string col = parts[0];
327327
string suffix = parts.Length > 1 ? " " + parts[1] : "";
328328

src/Eftdb/Internals/Features/Hypertables/HypertableModelExtractor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ public static IEnumerable<CreateHypertableOperation> GetHypertables(IRelationalM
6767
if (!string.IsNullOrWhiteSpace(orderByString))
6868
{
6969
compressionOrderBy = [];
70-
var clauses = orderByString.Split(',', StringSplitOptions.TrimEntries);
70+
string[] clauses = orderByString.Split(',', StringSplitOptions.TrimEntries);
7171

72-
foreach (var clause in clauses)
72+
foreach (string clause in clauses)
7373
{
7474
// Split by the first space to separate PropertyName from Directions (ASC/DESC/NULLS)
75-
var parts = clause.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
75+
string[] parts = clause.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
7676
if (parts.Length > 0)
7777
{
7878
string propName = parts[0];

tests/Eftdb.Tests/Integration/HypertableIntegrationTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ public async Task Should_Create_Hypertable_With_CompressionSegmentBy()
452452

453453
List<CompressionSettingInfo> settings = await GetCompressionSettingsAsync(context, "segment_by_metrics");
454454

455-
var tenantSetting = settings.FirstOrDefault(s => s.ColumnName == "TenantId");
455+
CompressionSettingInfo? tenantSetting = settings.FirstOrDefault(s => s.ColumnName == "TenantId");
456456
Assert.NotNull(tenantSetting);
457457

458458
Assert.Equal(1, tenantSetting.SegmentByIndex);
@@ -503,12 +503,12 @@ public async Task Should_Create_Hypertable_With_CompressionOrderBy()
503503
List<CompressionSettingInfo> settings = await GetCompressionSettingsAsync(context, "order_by_metrics");
504504

505505
// Verify Timestamp (DESC)
506-
var tsSetting = settings.First(s => s.ColumnName == "Timestamp");
506+
CompressionSettingInfo tsSetting = settings.First(s => s.ColumnName == "Timestamp");
507507
Assert.NotNull(tsSetting.OrderByIndex); // Should be ordered
508508
Assert.False(tsSetting.IsAscending); // DESC
509509

510510
// Verify Value (ASC, NULLS FIRST)
511-
var valSetting = settings.First(s => s.ColumnName == "Value");
511+
CompressionSettingInfo valSetting = settings.First(s => s.ColumnName == "Value");
512512
Assert.NotNull(valSetting.OrderByIndex);
513513
Assert.True(valSetting.IsAscending); // ASC (Default)
514514
Assert.True(valSetting.IsNullsFirst); // NULLS FIRST
@@ -554,11 +554,11 @@ public async Task Should_Create_Hypertable_With_FullCompressionSettings()
554554
List<CompressionSettingInfo> settings = await GetCompressionSettingsAsync(context, "full_comp_metrics");
555555

556556
// DeviceId should be Segment #1
557-
var deviceSetting = settings.First(s => s.ColumnName == "DeviceId");
557+
CompressionSettingInfo deviceSetting = settings.First(s => s.ColumnName == "DeviceId");
558558
Assert.Equal(1, deviceSetting.SegmentByIndex);
559559

560560
// Timestamp should be Order #1 (DESC)
561-
var tsSetting = settings.First(s => s.ColumnName == "Timestamp");
561+
CompressionSettingInfo tsSetting = settings.First(s => s.ColumnName == "Timestamp");
562562
Assert.Equal(1, tsSetting.OrderByIndex);
563563
Assert.False(tsSetting.IsAscending);
564564
}

tests/Eftdb.Tests/Integration/HypertableScaffoldingExtractorTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public async Task Should_Extract_CompressionSegmentBy()
214214
Dictionary<(string Schema, string TableName), object> result = extractor.Extract(connection);
215215

216216
Assert.Single(result);
217-
var info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
217+
HypertableScaffoldingExtractor.HypertableInfo info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
218218

219219
// Compression should be enabled
220220
Assert.True(info.CompressionEnabled);
@@ -268,14 +268,14 @@ public async Task Should_Extract_CompressionOrderBy()
268268
Dictionary<(string Schema, string TableName), object> result = extractor.Extract(connection);
269269

270270
Assert.Single(result);
271-
var info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
271+
HypertableScaffoldingExtractor.HypertableInfo info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
272272

273273
Assert.True(info.CompressionEnabled);
274274

275275
Assert.Equal(2, info.CompressionOrderBy.Count);
276276

277277
// Extractor reconstructs the string: "ColumnName [ASC|DESC] [NULLS FIRST|LAST]"
278-
Assert.Equal("Timestamp DESC NULLS FIRST", info.CompressionOrderBy[0]);
278+
Assert.Equal("Timestamp DESC", info.CompressionOrderBy[0]);
279279
// Note: Default for ASC is usually NULLS LAST in Postgres, but if we set NULLS FIRST explicitly:
280280
Assert.Equal("Value ASC NULLS FIRST", info.CompressionOrderBy[1]);
281281
}
@@ -322,7 +322,7 @@ public async Task Should_Extract_Full_Compression_Configuration()
322322
Dictionary<(string Schema, string TableName), object> result = extractor.Extract(connection);
323323

324324
Assert.Single(result);
325-
var info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
325+
HypertableScaffoldingExtractor.HypertableInfo info = (HypertableScaffoldingExtractor.HypertableInfo)result[("public", "Metrics")];
326326

327327
// SegmentBy
328328
Assert.Single(info.CompressionSegmentBy);

0 commit comments

Comments
 (0)