Skip to content

Commit c80e693

Browse files
Merge pull request #20 from anton-schieber/feature/edition-support
src: generator: hypertable: edition support
2 parents 5466335 + c7edb5d commit c80e693

File tree

8 files changed

+224
-71
lines changed

8 files changed

+224
-71
lines changed

benchmarks/Eftdb.Benchmarks/WriteRecordsBenchmarkBase.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ public async Task GlobalCleanup()
4646
public void IterationSetup()
4747
{
4848
Trades.Clear();
49-
var random = new Random();
49+
Random random = new();
5050
string[] tickers = ["AAPL", "GOOGL", "MSFT", "TSLA", "AMZN"];
51-
var baseTimestamp = DateTime.UtcNow.AddMinutes(-30);
51+
DateTime baseTimestamp = DateTime.UtcNow.AddMinutes(-30);
5252

5353
for (int i = 0; i < NumberOfRecords; i++)
5454
{
55-
var trade = CreateTradeInstance(i, baseTimestamp, tickers[random.Next(tickers.Length)], random);
55+
T trade = CreateTradeInstance(i, baseTimestamp, tickers[random.Next(tickers.Length)], random);
5656
Trades.Add(trade);
5757
}
5858

src/Eftdb.Design/TimescaleDatabaseModelFactory.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ public override DatabaseModel Create(DbConnection connection, DatabaseModelFacto
2828
DatabaseModel databaseModel = base.Create(connection, options);
2929

3030
// Extract all TimescaleDB features from the database
31-
var allFeatureData = _features
32-
.Select(feature => feature.Extractor.Extract(connection))
33-
.ToList();
31+
List<Dictionary<(string Schema, string TableName), object>> allFeatureData = [.. _features.Select(feature => feature.Extractor.Extract(connection))];
3432

3533
// Apply annotations to tables/views in the model
3634
foreach (DatabaseTable table in databaseModel.Tables)
@@ -42,7 +40,7 @@ public override DatabaseModel Create(DbConnection connection, DatabaseModelFacto
4240
// Apply each feature's annotations if the table has that feature
4341
for (int i = 0; i < _features.Count; i++)
4442
{
45-
var featureData = allFeatureData[i];
43+
Dictionary<(string Schema, string TableName), object> featureData = allFeatureData[i];
4644
if (featureData.TryGetValue(tableKey, out object? featureInfo))
4745
{
4846
_features[i].Applier.ApplyAnnotations(table, featureInfo);

src/Eftdb/Abstractions/TimescaleCopyConfig.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ public TimescaleCopyConfig()
5858
if (MapClrTypeToNpgsqlDbType(property.PropertyType, out NpgsqlDbType dbType))
5959
{
6060
// Auto-discover properties and create compiled getters for them.
61-
var parameter = Expression.Parameter(typeof(T), "x");
62-
var member = Expression.Property(parameter, property);
63-
var conversion = Expression.Convert(member, typeof(object));
64-
var lambda = Expression.Lambda<Func<T, object>>(conversion, parameter);
61+
ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
62+
MemberExpression member = Expression.Property(parameter, property);
63+
UnaryExpression conversion = Expression.Convert(member, typeof(object));
64+
Expression<Func<T, object>> lambda = Expression.Lambda<Func<T, object>>(conversion, parameter);
6565

6666
ColumnMappings[property.Name] = (lambda.Compile(), dbType);
6767
}
@@ -124,7 +124,7 @@ public TimescaleCopyConfig<T> MapColumn(string columnName, Expression<Func<T, ob
124124
private static bool MapClrTypeToNpgsqlDbType(Type clrType, out NpgsqlDbType dbType)
125125
{
126126
// Handle nullable value types by getting the underlying type
127-
var underlyingType = Nullable.GetUnderlyingType(clrType) ?? clrType;
127+
Type underlyingType = Nullable.GetUnderlyingType(clrType) ?? clrType;
128128

129129
// This map contains the default CLR type to NpgsqlDbType mappings
130130
// based on the Npgsql "Write Mappings" documentation.

src/Eftdb/Generators/ContinuousAggregateOperationGenerator.cs

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

34
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
45
{
@@ -114,7 +115,7 @@ public List<string> Generate(CreateContinuousAggregateOperation operation)
114115
}
115116

116117
// Build the complete CREATE MATERIALIZED VIEW statement as a single string
117-
var sqlBuilder = new System.Text.StringBuilder();
118+
StringBuilder sqlBuilder = new();
118119
sqlBuilder.Append($"CREATE MATERIALIZED VIEW {qualifiedIdentifier}");
119120
sqlBuilder.AppendLine();
120121
sqlBuilder.Append($"WITH ({string.Join(", ", withOptions)}) AS");

src/Eftdb/Generators/HypertableOperationGenerator.cs

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
22
using CmdScale.EntityFrameworkCore.TimescaleDB.Operations;
3+
using System.Text;
34

45
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
56
{
@@ -16,56 +17,58 @@ public HypertableOperationGenerator(bool isDesignTime = false)
1617
}
1718

1819
sqlHelper = new SqlBuilderHelper(quoteString);
19-
2020
}
2121

2222
public List<string> Generate(CreateHypertableOperation operation)
2323
{
2424
string qualifiedTableName = sqlHelper.Regclass(operation.TableName, operation.Schema);
2525
string qualifiedIdentifier = sqlHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
2626

27-
string migrateDataParam = operation.MigrateData ? ", migrate_data => true" : "";
27+
List<string> statements = [];
28+
List<string> communityStatements = [];
2829

29-
List<string> statements =
30-
[
31-
$"SELECT create_hypertable({qualifiedTableName}, '{operation.TimeColumnName}'{migrateDataParam});"
32-
];
30+
// Build create_hypertable with chunk_time_interval if provided
31+
StringBuilder createHypertableCall = new();
32+
createHypertableCall.Append($"SELECT create_hypertable({qualifiedTableName}, '{operation.TimeColumnName}'");
33+
createHypertableCall.Append(operation.MigrateData ? ", migrate_data => true" : "");
3334

34-
// ChunkTimeInterval
3535
if (!string.IsNullOrEmpty(operation.ChunkTimeInterval))
3636
{
3737
// Check if the interval is a plain number (e.g., for microseconds).
3838
if (long.TryParse(operation.ChunkTimeInterval, out _))
3939
{
4040
// If it's a number, don't wrap it in quotes.
41-
statements.Add($"SELECT set_chunk_time_interval({qualifiedTableName}, {operation.ChunkTimeInterval}::bigint);");
41+
createHypertableCall.Append($", chunk_time_interval => {operation.ChunkTimeInterval}::bigint");
4242
}
4343
else
4444
{
4545
// If it's a string like '7 days', wrap it in quotes.
46-
statements.Add($"SELECT set_chunk_time_interval({qualifiedTableName}, INTERVAL '{operation.ChunkTimeInterval}');");
46+
createHypertableCall.Append($", chunk_time_interval => INTERVAL '{operation.ChunkTimeInterval}'");
4747
}
4848
}
4949

50-
// EnableCompression
50+
createHypertableCall.Append(");");
51+
statements.Add(createHypertableCall.ToString());
52+
53+
// EnableCompression (Community Edition only)
5154
if (operation.EnableCompression || operation.ChunkSkipColumns?.Count > 0)
5255
{
5356
bool enableCompression = operation.EnableCompression || operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0;
54-
statements.Add($"ALTER TABLE {qualifiedIdentifier} SET (timescaledb.compress = {enableCompression.ToString().ToLower()});");
57+
communityStatements.Add($"ALTER TABLE {qualifiedIdentifier} SET (timescaledb.compress = {enableCompression.ToString().ToLower()});");
5558
}
5659

57-
// ChunkSkipColumns
60+
// ChunkSkipColumns (Community Edition only)
5861
if (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0)
5962
{
60-
statements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");
63+
communityStatements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");
6164

6265
foreach (string column in operation.ChunkSkipColumns)
6366
{
64-
statements.Add($"SELECT enable_chunk_skipping({qualifiedTableName}, '{column}');");
67+
communityStatements.Add($"SELECT enable_chunk_skipping({qualifiedTableName}, '{column}');");
6568
}
6669
}
6770

68-
// AdditionalDimensions
71+
// AdditionalDimensions (Available in both editions)
6972
if (operation.AdditionalDimensions != null && operation.AdditionalDimensions.Count > 0)
7073
{
7174
foreach (Dimension dimension in operation.AdditionalDimensions)
@@ -87,6 +90,11 @@ public List<string> Generate(CreateHypertableOperation operation)
8790
}
8891
}
8992

93+
// Add wrapped community statements if any exist
94+
if (communityStatements.Count > 0)
95+
{
96+
statements.Add(WrapCommunityFeatures(communityStatements));
97+
}
9098
return statements;
9199
}
92100

@@ -96,45 +104,52 @@ public List<string> Generate(AlterHypertableOperation operation)
96104
string qualifiedIdentifier = sqlHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
97105

98106
List<string> statements = [];
107+
List<string> communityStatements = [];
99108

100-
// Check for ChunkTimeInterval change
109+
// Check for ChunkTimeInterval change (Available in both editions)
101110
if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval)
102111
{
112+
StringBuilder setChunkTimeInterval = new();
113+
setChunkTimeInterval.Append($"SELECT set_chunk_time_interval({qualifiedTableName}, ");
114+
103115
// Check if the interval is a plain number (e.g., for microseconds).
104116
if (long.TryParse(operation.ChunkTimeInterval, out _))
105117
{
106118
// If it's a number, don't wrap it in quotes.
107-
statements.Add($"SELECT set_chunk_time_interval({qualifiedTableName}, {operation.ChunkTimeInterval}::bigint);");
119+
setChunkTimeInterval.Append($"{operation.ChunkTimeInterval}::bigint");
108120
}
109121
else
110122
{
111123
// If it's a string like '7 days', wrap it in quotes.
112-
statements.Add($"SELECT set_chunk_time_interval({qualifiedTableName}, INTERVAL '{operation.ChunkTimeInterval}');");
124+
setChunkTimeInterval.Append($"INTERVAL '{operation.ChunkTimeInterval}'");
113125
}
126+
127+
setChunkTimeInterval.Append(");");
128+
statements.Add(setChunkTimeInterval.ToString());
114129
}
115130

116-
// Check for EnableCompression change
131+
// Check for EnableCompression change (Community Edition only)
117132
bool newCompressionState = operation.EnableCompression || operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Any();
118133
bool oldCompressionState = operation.OldEnableCompression || operation.OldChunkSkipColumns != null && operation.OldChunkSkipColumns.Any();
119134

120135
if (newCompressionState != oldCompressionState)
121136
{
122137
string compressionValue = newCompressionState.ToString().ToLower();
123-
statements.Add($"ALTER TABLE {qualifiedIdentifier} SET (timescaledb.compress = {compressionValue});");
138+
communityStatements.Add($"ALTER TABLE {qualifiedIdentifier} SET (timescaledb.compress = {compressionValue});");
124139
}
125140

126-
// Handle ChunkSkipColumns
141+
// Handle ChunkSkipColumns (Community Edition only)
127142
IReadOnlyList<string> newColumns = operation.ChunkSkipColumns ?? [];
128143
IReadOnlyList<string> oldColumns = operation.OldChunkSkipColumns ?? [];
129144
List<string> addedColumns = [.. newColumns.Except(oldColumns)];
130145

131146
if (addedColumns.Count != 0)
132147
{
133-
statements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");
148+
communityStatements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");
134149

135150
foreach (string column in addedColumns)
136151
{
137-
statements.Add($"SELECT enable_chunk_skipping({qualifiedTableName}, '{column}');");
152+
communityStatements.Add($"SELECT enable_chunk_skipping({qualifiedTableName}, '{column}');");
138153
}
139154
}
140155

@@ -143,7 +158,7 @@ public List<string> Generate(AlterHypertableOperation operation)
143158
{
144159
foreach (string column in removedColumns)
145160
{
146-
statements.Add($"SELECT disable_chunk_skipping({qualifiedTableName}, '{column}');");
161+
communityStatements.Add($"SELECT disable_chunk_skipping({qualifiedTableName}, '{column}');");
147162
}
148163
}
149164

@@ -194,8 +209,41 @@ public List<string> Generate(AlterHypertableOperation operation)
194209
statements.Add($"-- WARNING: TimescaleDB does not support removing dimensions. The following dimensions cannot be removed: {dimensionList}");
195210
}
196211

212+
// Add wrapped community statements if any exist
213+
if (communityStatements.Count > 0)
214+
{
215+
statements.Add(WrapCommunityFeatures(communityStatements));
216+
}
197217
return statements;
198218
}
199-
}
200-
}
201219

220+
/// <summary>
221+
/// Wraps multiple SQL statements in a single license check block to ensure they only run on Community Edition.
222+
/// </summary>
223+
private static string WrapCommunityFeatures(List<string> sqlStatements)
224+
{
225+
StringBuilder sb = new();
226+
sb.AppendLine("DO $$");
227+
sb.AppendLine("DECLARE");
228+
sb.AppendLine(" license TEXT;");
229+
sb.AppendLine("BEGIN");
230+
sb.AppendLine(" license := current_setting('timescaledb.license', true);");
231+
sb.AppendLine(" ");
232+
sb.AppendLine(" IF license IS NULL OR license != 'apache' THEN");
233+
234+
foreach (string sql in sqlStatements)
235+
{
236+
// Remove trailing semicolon and escape single quotes for EXECUTE
237+
string cleanSql = sql.TrimEnd(';').Replace("'", "''");
238+
sb.AppendLine($" EXECUTE '{cleanSql}';");
239+
}
240+
241+
sb.AppendLine(" ELSE");
242+
sb.AppendLine(" RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';");
243+
sb.AppendLine(" END IF;");
244+
sb.AppendLine("END $$;");
245+
246+
return sb.ToString();
247+
}
248+
}
249+
}

src/Eftdb/TimescaleDbCopyExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static async Task BulkCopyAsync<T>(
5555
await writer.StartRowAsync();
5656

5757
// Write each configured column in the specified order
58-
foreach (var (Getter, DbType) in config.ColumnMappings.Values)
58+
foreach ((Func<T, object?>? Getter, NpgsqlTypes.NpgsqlDbType DbType) in config.ColumnMappings.Values)
5959
{
6060
object? value = Getter(item);
6161
await writer.WriteAsync(value, DbType);

0 commit comments

Comments
 (0)