Skip to content

Commit c7edb5d

Browse files
feat: generator: hypertable: use EXECUTE for edition support (fixes #12)
Use EXECUTE statements inside DO $$ blocks to fix PL/pgSQL limitations where SELECT statements cannot return data without a destination. - Wrap Community-only features in runtime license check - Escape single quotes for EXECUTE string literals - Move chunk_time_interval into create_hypertable() parameters - Update warning message to reference Apache Edition
1 parent ce02d7d commit c7edb5d

File tree

8 files changed

+75
-65
lines changed

8 files changed

+75
-65
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: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public List<string> Generate(CreateHypertableOperation operation)
2828
List<string> communityStatements = [];
2929

3030
// Build create_hypertable with chunk_time_interval if provided
31-
var createHypertableCall = new StringBuilder();
31+
StringBuilder createHypertableCall = new();
3232
createHypertableCall.Append($"SELECT create_hypertable({qualifiedTableName}, '{operation.TimeColumnName}'");
3333
createHypertableCall.Append(operation.MigrateData ? ", migrate_data => true" : "");
3434

@@ -109,7 +109,7 @@ public List<string> Generate(AlterHypertableOperation operation)
109109
// Check for ChunkTimeInterval change (Available in both editions)
110110
if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval)
111111
{
112-
var setChunkTimeInterval = new StringBuilder();
112+
StringBuilder setChunkTimeInterval = new();
113113
setChunkTimeInterval.Append($"SELECT set_chunk_time_interval({qualifiedTableName}, ");
114114

115115
// Check if the interval is a plain number (e.g., for microseconds).
@@ -209,7 +209,6 @@ public List<string> Generate(AlterHypertableOperation operation)
209209
statements.Add($"-- WARNING: TimescaleDB does not support removing dimensions. The following dimensions cannot be removed: {dimensionList}");
210210
}
211211

212-
213212
// Add wrapped community statements if any exist
214213
if (communityStatements.Count > 0)
215214
{
@@ -219,11 +218,11 @@ public List<string> Generate(AlterHypertableOperation operation)
219218
}
220219

221220
/// <summary>
222-
/// Wraps multiple SQL statements in a single license check block to ensure they only run on Community Edition
221+
/// Wraps multiple SQL statements in a single license check block to ensure they only run on Community Edition.
223222
/// </summary>
224-
private string WrapCommunityFeatures(List<string> sqlStatements)
223+
private static string WrapCommunityFeatures(List<string> sqlStatements)
225224
{
226-
var sb = new StringBuilder();
225+
StringBuilder sb = new();
227226
sb.AppendLine("DO $$");
228227
sb.AppendLine("DECLARE");
229228
sb.AppendLine(" license TEXT;");
@@ -234,11 +233,13 @@ private string WrapCommunityFeatures(List<string> sqlStatements)
234233

235234
foreach (string sql in sqlStatements)
236235
{
237-
sb.AppendLine($" {sql}");
236+
// Remove trailing semicolon and escape single quotes for EXECUTE
237+
string cleanSql = sql.TrimEnd(';').Replace("'", "''");
238+
sb.AppendLine($" EXECUTE '{cleanSql}';");
238239
}
239240

240241
sb.AppendLine(" ELSE");
241-
sb.AppendLine(" RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Enterprise Edition';");
242+
sb.AppendLine(" RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';");
242243
sb.AppendLine(" END IF;");
243244
sb.AppendLine("END $$;");
244245

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);

tests/Eftdb.Tests/Generators/HypertableOperationGeneratorComprehensiveTests.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ public void DesignTime_Create_CompressionWithoutChunkSkipping_GeneratesCorrectCo
143143
license TEXT;
144144
BEGIN
145145
license := current_setting('timescaledb.license', true);
146-
146+
147147
IF license IS NULL OR license != 'apache' THEN
148-
ALTER TABLE """"public"""".""""compressed_data"""" SET (timescaledb.compress = true);
148+
EXECUTE 'ALTER TABLE """"public"""".""""compressed_data"""" SET (timescaledb.compress = true)';
149149
ELSE
150-
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Enterprise Edition';
150+
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';
151151
END IF;
152152
END $$;
153153
"")";
@@ -429,11 +429,11 @@ public void DesignTime_Alter_DisablingCompression_GeneratesCorrectCode()
429429
license TEXT;
430430
BEGIN
431431
license := current_setting('timescaledb.license', true);
432-
432+
433433
IF license IS NULL OR license != 'apache' THEN
434-
ALTER TABLE """"public"""".""""decompress"""" SET (timescaledb.compress = false);
434+
EXECUTE 'ALTER TABLE """"public"""".""""decompress"""" SET (timescaledb.compress = false)';
435435
ELSE
436-
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Enterprise Edition';
436+
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';
437437
END IF;
438438
END $$;
439439
"")";
@@ -463,13 +463,13 @@ public void DesignTime_Alter_AddingChunkSkipColumn_GeneratesCorrectSequence()
463463
license TEXT;
464464
BEGIN
465465
license := current_setting('timescaledb.license', true);
466-
466+
467467
IF license IS NULL OR license != 'apache' THEN
468-
SET timescaledb.enable_chunk_skipping = 'ON';
469-
SELECT enable_chunk_skipping('public.""""add_skip""""', 'col2');
470-
SELECT enable_chunk_skipping('public.""""add_skip""""', 'col3');
468+
EXECUTE 'SET timescaledb.enable_chunk_skipping = ''ON''';
469+
EXECUTE 'SELECT enable_chunk_skipping(''public.""""add_skip""""'', ''col2'')';
470+
EXECUTE 'SELECT enable_chunk_skipping(''public.""""add_skip""""'', ''col3'')';
471471
ELSE
472-
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Enterprise Edition';
472+
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';
473473
END IF;
474474
END $$;
475475
"")";
@@ -499,11 +499,11 @@ public void DesignTime_Alter_RemovingChunkSkipColumn_GeneratesDisableCommands()
499499
license TEXT;
500500
BEGIN
501501
license := current_setting('timescaledb.license', true);
502-
502+
503503
IF license IS NULL OR license != 'apache' THEN
504-
SELECT disable_chunk_skipping('public.""""remove_skip""""', 'remove_this');
504+
EXECUTE 'SELECT disable_chunk_skipping(''public.""""remove_skip""""'', ''remove_this'')';
505505
ELSE
506-
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Enterprise Edition';
506+
RAISE WARNING 'Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition';
507507
END IF;
508508
END $$;
509509
"")";
@@ -592,8 +592,8 @@ public void Runtime_Alter_ChunkSkipping_RequiresSETCommand()
592592
string result = GetRuntimeSql(operation);
593593

594594
// Assert - Must SET enable_chunk_skipping = 'ON' before enable_chunk_skipping()
595-
Assert.Contains("SET timescaledb.enable_chunk_skipping = 'ON'", result);
596-
Assert.Contains("enable_chunk_skipping('public.\"skip_test\"', 'new_col')", result);
595+
Assert.Contains("SET timescaledb.enable_chunk_skipping = ''ON''", result);
596+
Assert.Contains("enable_chunk_skipping(''public.\"skip_test\"'', ''new_col'')", result);
597597
}
598598

599599
#endregion

0 commit comments

Comments
 (0)