Skip to content

Commit 84ba9ae

Browse files
fix: continuous aggregates now default create_group_indexes to true
1 parent 5ea665b commit 84ba9ae

21 files changed

Lines changed: 1045 additions & 154 deletions

‎docs/data-annotations/continuous-aggregates.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,20 @@ By default, continuous aggregates are populated with data when created. Set to `
207207

208208
### CreateGroupIndexes
209209

210-
Controls whether indexes are automatically created on GROUP BY columns. Enabled by default:
210+
Controls whether TimescaleDB creates indexes on the GROUP BY columns when the aggregate is created. Maps to the `timescaledb.create_group_indexes` option:
211+
212+
- **Unconfigured** — the `CreateGroupIndexes` property is never set — the option is omitted from `CREATE MATERIALIZED VIEW`, so TimescaleDB's server default applies.
213+
- Setting **`CreateGroupIndexes = true`** or **`CreateGroupIndexes = false`** emits the option explicitly.
211214

212215
```csharp
213216
[ContinuousAggregate(
214217
MaterializedViewName = "trade_stats",
215218
ParentName = nameof(Trade),
216-
CreateGroupIndexes = true)]
219+
CreateGroupIndexes = false)]
217220
```
218221

222+
> :warning: **Note:** `timescaledb.create_group_indexes` is a create-only option in TimescaleDB — `ALTER MATERIALIZED VIEW` rejects it. Only transitions to or from `false` are structural: because the server default is `true`, unconfigured and explicit `true` produce identical databases, so adding or removing a redundant `CreateGroupIndexes = true` is a no-op that never touches an existing aggregate. Changing the value to or from `false` on an **existing** aggregate drops and recreates it (see [Migration Ordering](../fluent-api/continuous-aggregates#migration-ordering)), which rematerializes it and, in a hierarchy, cascades the drop to every descendant. If the source hypertable's retention policy has already dropped the raw data covered by the aggregate, that history is lost on recreate. The flag is not recoverable from an existing database, so scaffolded aggregates never carry an explicit `CreateGroupIndexes` value.
223+
219224
### MaterializedOnly
220225

221226
By default, queries combine materialized data with recent unmaterialized data from the source hypertable. Set to `true` to return only pre-computed materialized data:

‎docs/fluent-api/continuous-aggregates.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ public class TradeDaily
285285
Ordering across the chain is handled automatically:
286286

287287
- Parents are created before their children; children are dropped before their parents.
288-
- A structural change to a parent (bucket width, bucket column name, aggregate functions, GROUP BY, or WHERE) drops and recreates all of its descendants as well, and their refresh policies are re-added afterwards.
288+
- A structural change to a parent (bucket width, bucket column name, aggregate functions, GROUP BY, WHERE, or the create-only create_group_indexes option) drops and recreates all of its descendants as well, and their refresh policies are re-added afterwards.
289289

290290
### Scaffolding
291291

@@ -310,7 +310,10 @@ builder.IsContinuousAggregate<TradeAggregate, Trade>(
310310

311311
### CreateGroupIndexes
312312

313-
Control whether indexes are automatically created on GROUP BY columns. Enabled by default:
313+
Controls whether TimescaleDB creates indexes on the GROUP BY columns when the aggregate is created. This maps to the `timescaledb.create_group_indexes` option:
314+
315+
- **Unconfigured** — no `.CreateGroupIndexes(...)` call — the option is omitted from `CREATE MATERIALIZED VIEW`, so TimescaleDB's server default applies.
316+
- **`.CreateGroupIndexes(true)`** and **`.CreateGroupIndexes(false)`** emit the option explicitly.
314317

315318
```csharp
316319
builder.IsContinuousAggregate<TradeAggregate, Trade>(
@@ -319,9 +322,13 @@ builder.IsContinuousAggregate<TradeAggregate, Trade>(
319322
x => x.Timestamp)
320323
.AddAggregateFunction(x => x.AveragePrice, x => x.Price, EAggregateFunction.Avg)
321324
.AddGroupByColumn(x => x.Ticker)
322-
.CreateGroupIndexes(true);
325+
.CreateGroupIndexes(false);
323326
```
324327

328+
> :warning: **Note:** In version <= `v10.3.0` an aggregate configured without a `.CreateGroupIndexes(...)` call was created with `timescaledb.create_group_indexes = false`, contradicting the server default. Unconfigured aggregates now omit the option and inherit the server default (`true`). Aggregates that need indexes suppressed must call `.CreateGroupIndexes(false)` explicitly.
329+
330+
> :warning: **Note:** `timescaledb.create_group_indexes` is a create-only option in TimescaleDB — `ALTER MATERIALIZED VIEW` rejects it. Only transitions to or from `false` are structural: because the server default is `true`, unconfigured and `.CreateGroupIndexes(true)` produce identical databases, so adding or removing a redundant `.CreateGroupIndexes(true)` call is a no-op that never touches an existing aggregate. Changing the value to or from `false` on an **existing** aggregate drops and recreates it (see [Migration Ordering](#migration-ordering)), which rematerializes it and, in a hierarchy, cascades the drop to every descendant. If the source hypertable's retention policy has already dropped the raw data covered by the aggregate, that history is lost on recreate. The flag is not recoverable from an existing database — it leaves only the indexes behind — so scaffolded aggregates never carry an explicit `.CreateGroupIndexes(...)` call.
331+
325332
### MaterializedOnly
326333

327334
By default, queries to a continuous aggregate combine materialized data with recent unmaterialized data from the source hypertable. Use `.MaterializedOnly()` to return only the pre-computed materialized data:

‎samples/Eftdb.Samples.Shared/Configurations/TradeAggregateConfiguration.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public void Configure(EntityTypeBuilder<TradeAggregate> builder)
2121
.AddGroupByColumn(x => x.Exchange)
2222
.AddGroupByColumn("1, 2")
2323
.Where("\"ticker\" = 'MCRS'")
24+
.CreateGroupIndexes(false)
2425
.MaterializedOnly()
2526
.WithRefreshPolicy(startOffset: "7 days", endOffset: "1 hour", scheduleInterval: "1 hour")
2627
.WithRefreshNewestFirst(true);

‎src/Eftdb.Design/Features/ContinuousAggregate/ContinuousAggregateAnnotationRenderer.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
5959
string? chunkInterval = GetString(annotations, ContinuousAggregateAnnotations.ChunkInterval);
6060
bool materializedOnly = Find(annotations, ContinuousAggregateAnnotations.MaterializedOnly)?.Value is true;
6161
bool withNoData = Find(annotations, ContinuousAggregateAnnotations.WithNoData)?.Value is true;
62-
bool createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool? ?? true;
62+
bool? createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool?;
6363

6464
ViewDefinitionParser.ParsedViewDefinition? parsed = viewDefinition is not null
6565
? ViewDefinitionParser.Parse(viewDefinition)
@@ -100,7 +100,7 @@ public IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
100100
call = call.Chain(WithNoDataMethod, true);
101101
}
102102

103-
if (!createGroupIndexes)
103+
if (createGroupIndexes is false)
104104
{
105105
call = call.Chain(CreateGroupIndexesMethod, false);
106106
}
@@ -172,7 +172,7 @@ public IReadOnlyList<AttributeCodeFragment> GenerateDataAnnotationAttributes(
172172
string? chunkInterval = GetString(annotations, ContinuousAggregateAnnotations.ChunkInterval);
173173
bool materializedOnly = Find(annotations, ContinuousAggregateAnnotations.MaterializedOnly)?.Value is true;
174174
bool withNoData = Find(annotations, ContinuousAggregateAnnotations.WithNoData)?.Value is true;
175-
bool createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool? ?? true;
175+
bool? createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool?;
176176

177177
ViewDefinitionParser.ParsedViewDefinition? parsed = viewDefinition is not null
178178
? ViewDefinitionParser.Parse(viewDefinition)
@@ -223,7 +223,7 @@ public IReadOnlyList<AttributeCodeFragment> GenerateDataAnnotationAttributes(
223223
caNamedArgs[nameof(ContinuousAggregateAttribute.WithNoData)] = true;
224224
}
225225

226-
if (!createGroupIndexes)
226+
if (createGroupIndexes is false)
227227
{
228228
caNamedArgs[nameof(ContinuousAggregateAttribute.CreateGroupIndexes)] = false;
229229
}

‎src/Eftdb.Design/Features/ContinuousAggregate/ContinuousAggregateCSharpGenerator.cs‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public void Generate(CreateContinuousAggregateOperation operation, IndentedStrin
2929
if (operation.WithNoData)
3030
call.Arg("withNoData", code.Literal(true));
3131

32-
if (operation.CreateGroupIndexes)
33-
call.Arg("createGroupIndexes", code.Literal(true));
32+
if (operation.CreateGroupIndexes.HasValue)
33+
call.Arg("createGroupIndexes", code.Literal(operation.CreateGroupIndexes.Value));
3434

3535
if (operation.MaterializedOnly)
3636
call.Arg("materializedOnly", code.Literal(true));
@@ -81,8 +81,6 @@ public void Generate(AlterContinuousAggregateOperation operation, IndentedString
8181
if (!string.IsNullOrEmpty(operation.ChunkInterval))
8282
call.Arg("chunkInterval", code.Literal(operation.ChunkInterval));
8383

84-
if (operation.CreateGroupIndexes)
85-
call.Arg("createGroupIndexes", code.Literal(true));
8684

8785
if (operation.MaterializedOnly)
8886
call.Arg("materializedOnly", code.Literal(true));
@@ -91,9 +89,6 @@ public void Generate(AlterContinuousAggregateOperation operation, IndentedString
9189
if (!string.IsNullOrEmpty(operation.OldChunkInterval))
9290
call.Arg("oldChunkInterval", code.Literal(operation.OldChunkInterval));
9391

94-
if (operation.OldCreateGroupIndexes)
95-
call.Arg("oldCreateGroupIndexes", code.Literal(true));
96-
9792
if (operation.OldMaterializedOnly)
9893
call.Arg("oldMaterializedOnly", code.Literal(true));
9994

‎src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateAttribute.cs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,20 @@ public class ContinuousAggregateAttribute : Attribute
3636

3737
/// <summary>
3838
/// Gets or sets a value indicating whether to automatically create indexes on the GROUP BY columns.
39-
/// Defaults to true. Corresponds to the 'timescaledb.create_group_indexes' option.
39+
/// Defaults to true. Corresponds to the create-only 'timescaledb.create_group_indexes' option.
40+
public bool CreateGroupIndexes
41+
{
42+
get => _createGroupIndexes ?? true;
43+
set => _createGroupIndexes = value;
44+
}
45+
46+
private bool? _createGroupIndexes;
47+
48+
/// <summary>
49+
/// The configured <see cref="CreateGroupIndexes"/> value, or <see langword="null"/> when the property
50+
/// was never set. The convention writes the annotation only when this has a value.
4051
/// </summary>
41-
public bool CreateGroupIndexes { get; set; } = true;
52+
internal bool? CreateGroupIndexesConfigured => _createGroupIndexes;
4253

4354
/// <summary>
4455
/// Gets or sets a value indicating whether queries to the view should only return materialized data.

‎src/Eftdb/Configuration/ContinuousAggregate/ContinuousAggregateConvention.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ public void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilde
2929
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.ParentName, continuousAggregateAttribute.ParentName);
3030
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.ChunkInterval, continuousAggregateAttribute.ChunkInterval);
3131
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.WithNoData, continuousAggregateAttribute.WithNoData);
32-
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.CreateGroupIndexes, continuousAggregateAttribute.CreateGroupIndexes);
32+
if (continuousAggregateAttribute.CreateGroupIndexesConfigured is bool createGroupIndexes)
33+
{
34+
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.CreateGroupIndexes, createGroupIndexes);
35+
}
3336
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.MaterializedOnly, continuousAggregateAttribute.MaterializedOnly);
3437
entityTypeBuilder.HasAnnotation(ContinuousAggregateAnnotations.WhereClause, continuousAggregateAttribute.Where);
3538

‎src/Eftdb/Generators/ContinuousAggregateSqlGenerator.cs‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ public static List<string> Generate(CreateContinuousAggregateOperation operation
2020
List<string> statements = [];
2121

2222
// Build WITH options
23-
List<string> withOptions =
24-
[
25-
"timescaledb.continuous",
26-
$"timescaledb.create_group_indexes = {operation.CreateGroupIndexes.ToString().ToLower()}",
27-
$"timescaledb.materialized_only = {operation.MaterializedOnly.ToString().ToLower()}"
28-
];
23+
List<string> withOptions = ["timescaledb.continuous"];
24+
25+
if (operation.CreateGroupIndexes.HasValue)
26+
{
27+
withOptions.Add($"timescaledb.create_group_indexes = {operation.CreateGroupIndexes.Value.ToString().ToLower()}");
28+
}
29+
30+
withOptions.Add($"timescaledb.materialized_only = {operation.MaterializedOnly.ToString().ToLower()}");
2931

3032
// Add optional chunk_interval if specified
3133
if (!string.IsNullOrEmpty(operation.ChunkInterval))
@@ -226,12 +228,6 @@ public static List<string> Generate(AlterContinuousAggregateOperation operation,
226228
}
227229
}
228230

229-
// Check for CreateGroupIndexes change
230-
if (operation.CreateGroupIndexes != operation.OldCreateGroupIndexes)
231-
{
232-
string createGroupIndexesValue = operation.CreateGroupIndexes.ToString().ToLower();
233-
statements.Add($"ALTER MATERIALIZED VIEW {qualifiedIdentifier} SET (timescaledb.create_group_indexes = {createGroupIndexesValue});");
234-
}
235231

236232
// Check for MaterializedOnly change
237233
if (operation.MaterializedOnly != operation.OldMaterializedOnly)

‎src/Eftdb/Internals/Features/ContinuousAggregates/ContinuousAggregateDiffer.cs‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,6 @@ public IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source
8787

8888
/// <summary>
8989
/// Find structural changes that require drop and recreate
90-
/// Note: Only certain properties can be altered (ChunkInterval, CreateGroupIndexes,
91-
/// MaterializedOnly, and compression settings).
92-
/// For structural changes (time bucket, aggregates, group by, where), drop and recreate is required.
9390
/// </summary>
9491
private static void FindStructuralChanges(
9592
List<CreateContinuousAggregateOperation> sourceAggregates,
@@ -113,6 +110,7 @@ private static void FindStructuralChanges(
113110
x.Target.TimeBucketColumnName != x.Source.TimeBucketColumnName ||
114111
x.Target.TimeBucketGroupBy != x.Source.TimeBucketGroupBy ||
115112
x.Target.WithNoData != x.Source.WithNoData ||
113+
(x.Target.CreateGroupIndexes ?? true) != (x.Source.CreateGroupIndexes ?? true) ||
116114
!AreAggregateFunctionsEqual(x.Target.AggregateFunctions, x.Source.AggregateFunctions) ||
117115
!AreGroupByColumnsEqual(x.Target.GroupByColumns, x.Source.GroupByColumns) ||
118116
x.Target.WhereClause != x.Source.WhereClause ||
@@ -134,9 +132,8 @@ private static void FindStructuralChanges(
134132

135133
/// <summary>
136134
/// Find changes limited to properties that can be applied in place (ChunkInterval,
137-
/// CreateGroupIndexes, MaterializedOnly, and compression settings) and emit alter operations.
138-
/// Aggregates already marked for drop and recreate are skipped; their recreated definition
139-
/// carries the new settings.
135+
/// MaterializedOnly, and compression settings) and emit alter operations. Aggregates already
136+
/// marked for drop and recreate are skipped; their recreated definition carries the new settings.
140137
/// </summary>
141138
private static void FindAlterableChanges(
142139
List<CreateContinuousAggregateOperation> sourceAggregates,
@@ -155,7 +152,6 @@ private static void FindAlterableChanges(
155152
!droppedNames.Contains(x.Target.MaterializedViewName) &&
156153
(
157154
x.Target.ChunkInterval != x.Source.ChunkInterval ||
158-
x.Target.CreateGroupIndexes != x.Source.CreateGroupIndexes ||
159155
x.Target.MaterializedOnly != x.Source.MaterializedOnly ||
160156
x.Target.EnableCompression != x.Source.EnableCompression ||
161157
!CompressionDiffHelper.AreStringListsEqual(x.Target.CompressionSegmentBy, x.Source.CompressionSegmentBy) ||
@@ -170,13 +166,11 @@ private static void FindAlterableChanges(
170166
Schema = aggregate.Target.Schema,
171167
MaterializedViewName = aggregate.Target.MaterializedViewName,
172168
ChunkInterval = aggregate.Target.ChunkInterval,
173-
CreateGroupIndexes = aggregate.Target.CreateGroupIndexes,
174169
MaterializedOnly = aggregate.Target.MaterializedOnly,
175170
EnableCompression = aggregate.Target.EnableCompression,
176171
CompressionSegmentBy = aggregate.Target.CompressionSegmentBy,
177172
CompressionOrderBy = aggregate.Target.CompressionOrderBy,
178173
OldChunkInterval = aggregate.Source.ChunkInterval,
179-
OldCreateGroupIndexes = aggregate.Source.CreateGroupIndexes,
180174
OldMaterializedOnly = aggregate.Source.MaterializedOnly,
181175
OldEnableCompression = aggregate.Source.EnableCompression,
182176
OldCompressionSegmentBy = aggregate.Source.CompressionSegmentBy,

‎src/Eftdb/Internals/Features/ContinuousAggregates/ContinuousAggregateModelExtractor.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private static IEnumerable<CreateContinuousAggregateOperation> ExtractContinuous
9797
bool timeBucketGroupBy = entityType.FindAnnotation(ContinuousAggregateAnnotations.TimeBucketGroupBy)?.Value as bool? ?? true;
9898
string? chunkInterval = entityType.FindAnnotation(ContinuousAggregateAnnotations.ChunkInterval)?.Value as string;
9999
bool withNoData = entityType.FindAnnotation(ContinuousAggregateAnnotations.WithNoData)?.Value as bool? ?? false;
100-
bool createGroupIndexes = entityType.FindAnnotation(ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool? ?? false;
100+
bool? createGroupIndexes = entityType.FindAnnotation(ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool?;
101101
bool materializedOnly = entityType.FindAnnotation(ContinuousAggregateAnnotations.MaterializedOnly)?.Value as bool? ?? false;
102102
string? whereClause = entityType.FindAnnotation(ContinuousAggregateAnnotations.WhereClause)?.Value as string;
103103

0 commit comments

Comments
 (0)