Skip to content

Commit 7ad1a5d

Browse files
Merge pull request #19 from cmdscale/fix/#14_fix_altering_space_dimensions
fix: enable altering hypertable dimensions via AlterHypertableOperation
2 parents b39bbf0 + ed889c0 commit 7ad1a5d

File tree

8 files changed

+88
-16
lines changed

8 files changed

+88
-16
lines changed

CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/HypertableOperationGeneratorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class HypertableOperationGeneratorTests
1414
private static string GetGeneratedCode(dynamic operation)
1515
{
1616
IndentedStringBuilder builder = new();
17-
17+
1818
HypertableOperationGenerator generator = new(true);
1919
List<string> statements = generator.Generate(operation);
2020
SqlBuilderHelper.BuildQueryString(statements, builder);

CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/ReorderPolicyOperationGeneratorTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ public void Generate_Add_with_non_default_schedule_creates_add_and_alter_sql()
5353
IndexName = "IX_TestTable_Time",
5454
InitialStart = testDate,
5555
ScheduleInterval = "2 days",
56-
MaxRuntime = "1 hour",
57-
MaxRetries = 5,
58-
RetryPeriod = "10 minutes"
56+
MaxRuntime = "1 hour",
57+
MaxRetries = 5,
58+
RetryPeriod = "10 minutes"
5959
};
6060

6161
string expected = @".Sql(@""
@@ -78,10 +78,10 @@ FROM timescaledb_information.jobs
7878
public void Generate_Drop_creates_correct_remove_policy_sql()
7979
{
8080
// Arrange
81-
DropReorderPolicyOperation operation = new()
82-
{
81+
DropReorderPolicyOperation operation = new()
82+
{
8383
Schema = "public",
84-
TableName = "TestTable"
84+
TableName = "TestTable"
8585
};
8686

8787
string expected = @".Sql(@""

CmdScale.EntityFrameworkCore.TimescaleDB/Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ EAggregateFunction function
9898

9999
public static ContinuousAggregateBuilder<TEntity, TSourceEntity> AddGroupByColumn<TEntity, TSourceEntity, TProperty>(
100100
this ContinuousAggregateBuilder<TEntity, TSourceEntity> builder,
101-
Expression<Func<TSourceEntity, TProperty>> propertyExpression)
102-
where TEntity : class
101+
Expression<Func<TSourceEntity, TProperty>> propertyExpression)
102+
where TEntity : class
103103
where TSourceEntity : class
104104
{
105105
string propertyName = GetPropertyName(propertyExpression);

CmdScale.EntityFrameworkCore.TimescaleDB/Configuration/Hypertable/HypertableAnnotations.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public static class HypertableAnnotations
88
public const string IsHypertable = "TimescaleDB:IsHypertable";
99
public const string HypertableTimeColumn = "TimescaleDB:TimeColumnName";
1010
public const string EnableCompression = "TimescaleDB:EnableCompression";
11-
public const string ChunkTimeInterval ="TimescaleDB:ChunkTimeInterval";
11+
public const string ChunkTimeInterval = "TimescaleDB:ChunkTimeInterval";
1212
public const string ChunkSkipColumns = "TimescaleDB:ChunkSkipColumns";
1313
public const string AdditionalDimensions = "TimescaleDB:AdditionalDimensions";
1414
}

CmdScale.EntityFrameworkCore.TimescaleDB/Generators/HypertableOperationGenerator.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,47 @@ public List<string> Generate(AlterHypertableOperation operation)
139139
}
140140
}
141141

142+
// Handle AdditionalDimensions - only add new dimensions
143+
// NOTE: TimescaleDB does NOT support removing dimensions from hypertables.
144+
// Once a dimension is added, it cannot be removed. Therefore, we only generate
145+
// SQL for adding new dimensions and ignore dimension removals.
146+
IReadOnlyList<Dimension> newDimensions = operation.AdditionalDimensions ?? [];
147+
IReadOnlyList<Dimension> oldDimensions = operation.OldAdditionalDimensions ?? [];
148+
149+
// Find dimensions that are in new but not in old (added dimensions)
150+
foreach (Dimension newDim in newDimensions)
151+
{
152+
bool exists = oldDimensions.Any(oldDim =>
153+
oldDim.ColumnName == newDim.ColumnName &&
154+
oldDim.Type == newDim.Type &&
155+
oldDim.Interval == newDim.Interval &&
156+
oldDim.NumberOfPartitions == newDim.NumberOfPartitions);
157+
158+
if (!exists)
159+
{
160+
if (newDim.Type == EDimensionType.Range)
161+
{
162+
statements.Add($"SELECT add_dimension({qualifiedTableName}, by_range('{newDim.ColumnName}', INTERVAL '{newDim.Interval}'));");
163+
}
164+
else if (newDim.Type == EDimensionType.Hash)
165+
{
166+
statements.Add($"SELECT add_dimension({qualifiedTableName}, by_hash('{newDim.ColumnName}', {newDim.NumberOfPartitions}));");
167+
}
168+
}
169+
}
170+
171+
// Warn if dimensions were removed (which cannot be reversed in TimescaleDB)
172+
List<Dimension> removedDimensions = [.. oldDimensions
173+
.Where(oldDim => !newDimensions.Any(newDim =>
174+
oldDim.ColumnName == newDim.ColumnName &&
175+
oldDim.Type == newDim.Type))];
176+
177+
if (removedDimensions.Count > 0)
178+
{
179+
string dimensionList = string.Join(", ", removedDimensions.Select(d => $"'{d.ColumnName}'"));
180+
statements.Add($"-- WARNING: TimescaleDB does not support removing dimensions. The following dimensions cannot be removed: {dimensionList}");
181+
}
182+
142183
return statements;
143184
}
144185
}

CmdScale.EntityFrameworkCore.TimescaleDB/Internals/Features/Hypertables/HypertableDiffer.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CmdScale.EntityFrameworkCore.TimescaleDB.Operations;
1+
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
2+
using CmdScale.EntityFrameworkCore.TimescaleDB.Operations;
23
using Microsoft.EntityFrameworkCore.Metadata;
34
using Microsoft.EntityFrameworkCore.Migrations.Operations;
45

@@ -28,7 +29,8 @@ public IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source
2829
.Where(x =>
2930
x.Target.ChunkTimeInterval != x.Source.ChunkTimeInterval ||
3031
x.Target.EnableCompression != x.Source.EnableCompression ||
31-
!AreChunkSkipColumnsEqual(x.Target.ChunkSkipColumns, x.Source.ChunkSkipColumns)
32+
!AreChunkSkipColumnsEqual(x.Target.ChunkSkipColumns, x.Source.ChunkSkipColumns) ||
33+
!AreDimensionsEqual(x.Target.AdditionalDimensions, x.Source.AdditionalDimensions)
3234
);
3335

3436
foreach (var hypertable in updatedHypertables)
@@ -40,9 +42,11 @@ public IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source
4042
ChunkTimeInterval = hypertable.Target.ChunkTimeInterval,
4143
EnableCompression = hypertable.Target.EnableCompression,
4244
ChunkSkipColumns = hypertable.Target.ChunkSkipColumns,
45+
AdditionalDimensions = hypertable.Target.AdditionalDimensions,
4346
OldChunkTimeInterval = hypertable.Source.ChunkTimeInterval,
4447
OldEnableCompression = hypertable.Source.EnableCompression,
45-
OldChunkSkipColumns = hypertable.Source.ChunkSkipColumns
48+
OldChunkSkipColumns = hypertable.Source.ChunkSkipColumns,
49+
OldAdditionalDimensions = hypertable.Source.AdditionalDimensions
4650
});
4751
}
4852

@@ -59,5 +63,29 @@ private static bool AreChunkSkipColumnsEqual(IReadOnlyList<string>? list1, IRead
5963

6064
return new HashSet<string>(list1).SetEquals(list2);
6165
}
66+
67+
private static bool AreDimensionsEqual(IReadOnlyList<Dimension>? list1, IReadOnlyList<Dimension>? list2)
68+
{
69+
if (list1 == null && list2 == null) return true;
70+
if (list1 == null || list2 == null) return false;
71+
if (list1.Count != list2.Count) return false;
72+
73+
// Compare each dimension's properties
74+
for (int i = 0; i < list1.Count; i++)
75+
{
76+
Dimension dim1 = list1[i];
77+
Dimension dim2 = list2[i];
78+
79+
if (dim1.ColumnName != dim2.ColumnName ||
80+
dim1.Type != dim2.Type ||
81+
dim1.Interval != dim2.Interval ||
82+
dim1.NumberOfPartitions != dim2.NumberOfPartitions)
83+
{
84+
return false;
85+
}
86+
}
87+
88+
return true;
89+
}
6290
}
6391
}

CmdScale.EntityFrameworkCore.TimescaleDB/Operations/AlterContinuousAggregateOperation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ public class AlterContinuousAggregateOperation : MigrationOperation
99

1010
public string? ChunkInterval { get; set; }
1111
public string? OldChunkInterval { get; set; }
12-
12+
1313
public bool CreateGroupIndexes { get; set; }
1414
public bool OldCreateGroupIndexes { get; set; }
15-
15+
1616
public bool MaterializedOnly { get; set; }
1717
public bool OldMaterializedOnly { get; set; }
1818
}

CmdScale.EntityFrameworkCore.TimescaleDB/Operations/AlterHypertableOperation.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.EntityFrameworkCore.Migrations.Operations;
1+
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
2+
using Microsoft.EntityFrameworkCore.Migrations.Operations;
23

34
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Operations
45
{
@@ -13,9 +14,11 @@ public class AlterHypertableOperation : MigrationOperation
1314
// Only timestamp-like and Integer-like columns are supported for chunk skipping
1415
// Cannot be reverted once enabled
1516
public IReadOnlyList<string>? ChunkSkipColumns { get; set; }
17+
public IReadOnlyList<Dimension>? AdditionalDimensions { get; set; }
1618

1719
public string OldChunkTimeInterval { get; set; } = string.Empty;
1820
public bool OldEnableCompression { get; set; }
1921
public IReadOnlyList<string>? OldChunkSkipColumns { get; set; }
22+
public IReadOnlyList<Dimension>? OldAdditionalDimensions { get; set; }
2023
}
2124
}

0 commit comments

Comments
 (0)