Skip to content

Commit cf125c1

Browse files
Merge pull request #9 from cmdscale/fix/#6_schema_support
fix: implement support for custom schemas for TimescaleDB operations
2 parents 8aaacdb + b3092f2 commit cf125c1

File tree

16 files changed

+128
-68
lines changed

16 files changed

+128
-68
lines changed

CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleCSharpModelGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public override DatabaseModel Create(DbConnection connection, DatabaseModelFacto
104104

105105
private static Dictionary<(string, string), HypertableInfo> GetHypertables(DbConnection connection)
106106
{
107-
bool wasOpen = connection.State == System.Data.ConnectionState.Open;
107+
bool wasOpen = connection.State == ConnectionState.Open;
108108
if (!wasOpen)
109109
{
110110
connection.Open();

CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess/TimescaleContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1616
{
1717
base.OnModelCreating(modelBuilder);
1818
modelBuilder.ApplyConfigurationsFromAssembly(typeof(TimescaleContext).Assembly);
19+
modelBuilder.HasDefaultSchema("custom_schema");
1920
}
2021
}
2122
}

CmdScale.EntityFrameworkCore.TimescaleDB.Example/Program.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@
1313

1414
IHost host = builder.Build();
1515

16+
#if false
17+
// --- Code to run Database.EnsureCreatedAsync() ---
18+
// NOTE: Set the #if to false to disable this block or to true to enable it.
19+
using (IServiceScope scope = host.Services.CreateScope())
20+
{
21+
IServiceProvider services = scope.ServiceProvider;
22+
try
23+
{
24+
TimescaleContext context = services.GetRequiredService<TimescaleContext>();
25+
Console.WriteLine("Applying Database.EnsureCreatedAsync()...");
26+
await context.Database.EnsureCreatedAsync();
27+
Console.WriteLine("Database setup complete.");
28+
}
29+
catch (Exception ex)
30+
{
31+
Console.WriteLine($"An error occurred while creating the database: {ex.Message}");
32+
}
33+
}
34+
#endif
35+
1636
Console.WriteLine("TimescaleDB EF Core Demo");
1737
Console.WriteLine("------------------------------------");
1838
Console.WriteLine("Run 'dotnet ef migrations add <MigrationName>' to generate a migration.");

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ public void Generate_Create_with_minimal_details_generates_correct_sql()
3030
CreateHypertableOperation operation = new()
3131
{
3232
TableName = "MinimalTable",
33+
Schema = "public",
3334
TimeColumnName = "Timestamp"
3435
};
3536

3637
string expected = @".Sql(@""
37-
SELECT create_hypertable('""""MinimalTable""""', 'Timestamp');
38+
SELECT create_hypertable('public.""""MinimalTable""""', 'Timestamp');
3839
"")";
3940

4041
// Act
@@ -51,6 +52,7 @@ public void Generate_Create_with_all_options_generates_comprehensive_sql()
5152
CreateHypertableOperation operation = new()
5253
{
5354
TableName = "FullTable",
55+
Schema = "custom_schema",
5456
TimeColumnName = "EventTime",
5557
ChunkTimeInterval = "1 day",
5658
EnableCompression = true,
@@ -62,12 +64,12 @@ public void Generate_Create_with_all_options_generates_comprehensive_sql()
6264
};
6365

6466
string expected = @".Sql(@""
65-
SELECT create_hypertable('""""FullTable""""', 'EventTime');
66-
SELECT set_chunk_time_interval('""""FullTable""""', INTERVAL '1 day');
67-
ALTER TABLE """"FullTable"""" SET (timescaledb.compress = true);
67+
SELECT create_hypertable('custom_schema.""""FullTable""""', 'EventTime');
68+
SELECT set_chunk_time_interval('custom_schema.""""FullTable""""', INTERVAL '1 day');
69+
ALTER TABLE """"custom_schema"""".""""FullTable"""" SET (timescaledb.compress = true);
6870
SET timescaledb.enable_chunk_skipping = 'ON';
69-
SELECT enable_chunk_skipping('""""FullTable""""', 'DeviceId');
70-
SELECT add_dimension('""""FullTable""""', by_hash('LocationId', 4));
71+
SELECT enable_chunk_skipping('custom_schema.""""FullTable""""', 'DeviceId');
72+
SELECT add_dimension('custom_schema.""""FullTable""""', by_hash('LocationId', 4));
7173
"")";
7274

7375
// Act
@@ -84,16 +86,17 @@ public void Generate_Alter_WhenAddingChunkSkippingToUncompressedTable_ShouldAlso
8486
AlterHypertableOperation operation = new()
8587
{
8688
TableName = "Metrics",
89+
Schema = "custom_schema",
8790
OldEnableCompression = false,
8891
OldChunkSkipColumns = [],
8992
EnableCompression = false,
9093
ChunkSkipColumns = ["device_id"]
9194
};
9295

9396
string expected = @".Sql(@""
94-
ALTER TABLE """"Metrics"""" SET (timescaledb.compress = true);
97+
ALTER TABLE """"custom_schema"""".""""Metrics"""" SET (timescaledb.compress = true);
9598
SET timescaledb.enable_chunk_skipping = 'ON';
96-
SELECT enable_chunk_skipping('""""Metrics""""', 'device_id');
99+
SELECT enable_chunk_skipping('custom_schema.""""Metrics""""', 'device_id');
97100
"")";
98101

99102
// Act
@@ -112,12 +115,13 @@ public void Generate_Alter_when_changing_compression_generates_correct_sql()
112115
AlterHypertableOperation operation = new()
113116
{
114117
TableName = "SensorData",
118+
Schema = "public",
115119
EnableCompression = true,
116120
OldEnableCompression = false
117121
};
118122

119123
string expected = @".Sql(@""
120-
ALTER TABLE """"SensorData"""" SET (timescaledb.compress = true);
124+
ALTER TABLE """"public"""".""""SensorData"""" SET (timescaledb.compress = true);
121125
"")";
122126

123127
// Act
@@ -134,14 +138,15 @@ public void Generate_Alter_when_adding_and_removing_skip_columns_generates_corre
134138
AlterHypertableOperation operation = new()
135139
{
136140
TableName = "Metrics",
141+
Schema = "metrics_schema",
137142
ChunkSkipColumns = ["host", "service"],
138143
OldChunkSkipColumns = ["host", "region"]
139144
};
140145

141146
string expected = @".Sql(@""
142147
SET timescaledb.enable_chunk_skipping = 'ON';
143-
SELECT enable_chunk_skipping('""""Metrics""""', 'service');
144-
SELECT disable_chunk_skipping('""""Metrics""""', 'region');
148+
SELECT enable_chunk_skipping('metrics_schema.""""Metrics""""', 'service');
149+
SELECT disable_chunk_skipping('metrics_schema.""""Metrics""""', 'region');
145150
"")";
146151

147152
// Act
@@ -158,6 +163,7 @@ public void Generate_Alter_when_no_properties_change_generates_no_sql()
158163
AlterHypertableOperation operation = new()
159164
{
160165
TableName = "NoChangeTable",
166+
Schema = "public",
161167
EnableCompression = true,
162168
OldEnableCompression = true,
163169
ChunkTimeInterval = "7 days",
@@ -180,14 +186,15 @@ public void Generate_Alter_WhenRemovingLastChunkSkipColumn_ShouldDisableCompress
180186
AlterHypertableOperation operation = new()
181187
{
182188
TableName = "Logs",
189+
Schema = "public",
183190
OldEnableCompression = false,
184191
OldChunkSkipColumns = ["trace_id"],
185192
EnableCompression = false,
186193
ChunkSkipColumns = []
187194
};
188195
string expected = @".Sql(@""
189-
ALTER TABLE """"Logs"""" SET (timescaledb.compress = false);
190-
SELECT disable_chunk_skipping('""""Logs""""', 'trace_id');
196+
ALTER TABLE """"public"""".""""Logs"""" SET (timescaledb.compress = false);
197+
SELECT disable_chunk_skipping('public.""""Logs""""', 'trace_id');
191198
"")";
192199

193200
// Act

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ public void Generate_Add_with_minimal_details_creates_only_add_policy_sql()
2525
// Arrange
2626
AddReorderPolicyOperation operation = new()
2727
{
28+
Schema = "public",
2829
TableName = "TestTable",
2930
IndexName = "IX_TestTable_Time"
3031
};
3132

3233
string expected = @".Sql(@""
33-
SELECT add_reorder_policy('""""TestTable""""', 'IX_TestTable_Time');
34+
SELECT add_reorder_policy('public.""""TestTable""""', 'IX_TestTable_Time');
3435
"")";
3536

3637
// Act
@@ -47,20 +48,21 @@ public void Generate_Add_with_non_default_schedule_creates_add_and_alter_sql()
4748
DateTime testDate = new(2025, 10, 20, 12, 30, 0, DateTimeKind.Utc);
4849
AddReorderPolicyOperation operation = new()
4950
{
51+
Schema = "custom",
5052
TableName = "TestTable",
5153
IndexName = "IX_TestTable_Time",
5254
InitialStart = testDate,
5355
ScheduleInterval = "2 days",
54-
MaxRuntime = "1 hour",
56+
MaxRuntime = "1 hour",
5557
MaxRetries = 5,
5658
RetryPeriod = "10 minutes"
5759
};
5860

5961
string expected = @".Sql(@""
60-
SELECT add_reorder_policy('""""TestTable""""', 'IX_TestTable_Time', initial_start => '2025-10-20T12:30:00.0000000Z');
62+
SELECT add_reorder_policy('custom.""""TestTable""""', 'IX_TestTable_Time', initial_start => '2025-10-20T12:30:00.0000000Z');
6163
SELECT alter_job(job_id, schedule_interval => INTERVAL '2 days', max_runtime => INTERVAL '1 hour', max_retries => 5, retry_period => INTERVAL '10 minutes')
6264
FROM timescaledb_information.jobs
63-
WHERE proc_name = 'policy_reorder' AND hypertable_name = 'TestTable';
65+
WHERE proc_name = 'policy_reorder' AND hypertable_schema = 'custom' AND hypertable_name = 'TestTable';
6466
"")";
6567

6668
// Act
@@ -76,10 +78,14 @@ FROM timescaledb_information.jobs
7678
public void Generate_Drop_creates_correct_remove_policy_sql()
7779
{
7880
// Arrange
79-
DropReorderPolicyOperation operation = new() { TableName = "TestTable" };
81+
DropReorderPolicyOperation operation = new()
82+
{
83+
Schema = "public",
84+
TableName = "TestTable"
85+
};
8086

8187
string expected = @".Sql(@""
82-
SELECT remove_reorder_policy('""""TestTable""""', if_exists => true);
88+
SELECT remove_reorder_policy('public.""""TestTable""""', if_exists => true);
8389
"")";
8490

8591
// Act
@@ -97,6 +103,7 @@ public void Generate_Alter_when_only_job_settings_change_creates_only_alter_job_
97103
// Arrange
98104
AlterReorderPolicyOperation operation = new()
99105
{
106+
Schema = "metrics",
100107
TableName = "TestTable",
101108
// Fundamental properties are the same
102109
IndexName = "IX_TestTable_Time",
@@ -111,7 +118,7 @@ public void Generate_Alter_when_only_job_settings_change_creates_only_alter_job_
111118
string expected = @".Sql(@""
112119
SELECT alter_job(job_id, schedule_interval => INTERVAL '2 days')
113120
FROM timescaledb_information.jobs
114-
WHERE proc_name = 'policy_reorder' AND hypertable_name = 'TestTable';
121+
WHERE proc_name = 'policy_reorder' AND hypertable_schema = 'metrics' AND hypertable_name = 'TestTable';
115122
"")";
116123

117124
// Act
@@ -127,6 +134,7 @@ public void Generate_Alter_when_fundamental_property_changes_creates_drop_and_ad
127134
// Arrange
128135
AlterReorderPolicyOperation operation = new()
129136
{
137+
Schema = "logs",
130138
TableName = "TestTable",
131139
IndexName = "IX_New_Name",
132140
OldIndexName = "IX_Old_Name",
@@ -135,11 +143,11 @@ public void Generate_Alter_when_fundamental_property_changes_creates_drop_and_ad
135143
};
136144

137145
string expected = @".Sql(@""
138-
SELECT remove_reorder_policy('""""TestTable""""', if_exists => true);
139-
SELECT add_reorder_policy('""""TestTable""""', 'IX_New_Name');
146+
SELECT remove_reorder_policy('logs.""""TestTable""""', if_exists => true);
147+
SELECT add_reorder_policy('logs.""""TestTable""""', 'IX_New_Name');
140148
SELECT alter_job(job_id, schedule_interval => INTERVAL '2 days')
141149
FROM timescaledb_information.jobs
142-
WHERE proc_name = 'policy_reorder' AND hypertable_name = 'TestTable';
150+
WHERE proc_name = 'policy_reorder' AND hypertable_schema = 'logs' AND hypertable_name = 'TestTable';
143151
"")";
144152

145153
// Act
@@ -155,6 +163,7 @@ public void Generate_Alter_when_both_fundamental_and_job_settings_change_creates
155163
// Arrange
156164
AlterReorderPolicyOperation operation = new()
157165
{
166+
Schema = "public",
158167
TableName = "TestTable",
159168
IndexName = "IX_New_Name",
160169
OldIndexName = "IX_Old_Name",
@@ -167,11 +176,11 @@ public void Generate_Alter_when_both_fundamental_and_job_settings_change_creates
167176
};
168177

169178
string expected = @".Sql(@""
170-
SELECT remove_reorder_policy('""""TestTable""""', if_exists => true);
171-
SELECT add_reorder_policy('""""TestTable""""', 'IX_New_Name');
179+
SELECT remove_reorder_policy('public.""""TestTable""""', if_exists => true);
180+
SELECT add_reorder_policy('public.""""TestTable""""', 'IX_New_Name');
172181
SELECT alter_job(job_id, schedule_interval => INTERVAL '2 days', max_retries => 5, retry_period => INTERVAL '10 minutes')
173182
FROM timescaledb_information.jobs
174-
WHERE proc_name = 'policy_reorder' AND hypertable_name = 'TestTable';
183+
WHERE proc_name = 'policy_reorder' AND hypertable_schema = 'public' AND hypertable_name = 'TestTable';
175184
"")";
176185

177186
// Act
@@ -181,4 +190,4 @@ FROM timescaledb_information.jobs
181190
Assert.Equal(SqlHelper.NormalizeSql(expected), SqlHelper.NormalizeSql(result));
182191
}
183192
}
184-
}
193+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void Regclass_Runtime_ReturnsCorrectlyQuotedString()
1919
// Arrange
2020
SqlBuilderHelper helper = new(quoteString: "\"");
2121
string tableName = "MyTable";
22-
string expected = "'\"MyTable\"'";
22+
string expected = "'public.\"MyTable\"'";
2323

2424
// Act
2525
string result = helper.Regclass(tableName);
@@ -34,7 +34,7 @@ public void QualifiedIdentifier_Runtime_ReturnsCorrectlyQuotedString()
3434
// Arrange
3535
SqlBuilderHelper helper = new(quoteString: "\"");
3636
string tableName = "MyTable";
37-
string expected = "\"MyTable\"";
37+
string expected = "\"public\".\"MyTable\"";
3838

3939
// Act
4040
string result = helper.QualifiedIdentifier(tableName);
@@ -49,7 +49,7 @@ public void Regclass_DesignTime_ReturnsCorrectlyEscapedQuotedString()
4949
// Arrange
5050
SqlBuilderHelper helper = new(quoteString: "\"\"");
5151
string tableName = "MyTable";
52-
string expected = "'\"\"MyTable\"\"'";
52+
string expected = "'public.\"\"MyTable\"\"'";
5353

5454
// Act
5555
string result = helper.Regclass(tableName);
@@ -64,7 +64,7 @@ public void QualifiedIdentifier_DesignTime_ReturnsCorrectlyEscapedQuotedString()
6464
// Arrange
6565
SqlBuilderHelper helper = new(quoteString: "\"\"");
6666
string tableName = "MyTable";
67-
string expected = "\"\"MyTable\"\"";
67+
string expected = "\"\"public\"\".\"\"MyTable\"\"";
6868

6969
// Act
7070
string result = helper.QualifiedIdentifier(tableName);

CmdScale.EntityFrameworkCore.TimescaleDB/DefaultValues.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/// </summary>
66
public static class DefaultValues
77
{
8+
public const string DefaultSchema = "public";
89
public const string ChunkTimeInterval = "7 days";
910
public const long ChunkTimeIntervalLong = 604_800_000_000L;
1011
public const string ReorderPolicyScheduleInterval = "1 day";

0 commit comments

Comments
 (0)