Skip to content

Commit d500beb

Browse files
feat: add UseApacheEdition() option for Apache 2 (OSS) edition support
1 parent cbb588c commit d500beb

56 files changed

Lines changed: 1747 additions & 1118 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/CONTRIBUTING.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ For convenient local development, a `docker-compose.yml` file is included in the
8787

8888
Also, some tests use `Testcontainers` and need you to have Docker installed. Just keep that in mind.
8989

90+
#### Switching between TimescaleDB editions
91+
92+
TimescaleDB ships in two editions: the **Community edition** (default image, full feature set) and the **Apache 2 edition** (`-oss` image tags, no columnstore/compression, continuous aggregates, or background policies). The library supports both (see `docs/05-apache-edition.md`), so manual testing against the Apache edition is sometimes needed.
93+
94+
The auto-loaded `docker-compose.override.yml` adds a `db-apache` service for this. Both editions bind the same port `5432` with identical credentials.
95+
96+
> [!WARNING]
97+
> Each edition has its own data volume, and they are **not interchangeable**
98+
9099
### 🧪 Testing
91100

92101
This project uses a two-tier testing strategy to ensure code quality and correctness.

‎docker-compose.override.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
db-apache:
3+
image: timescale/timescaledb:latest-pg17-oss
4+
container_name: cmdscale-ef-timescaledb-apache
5+
restart: always
6+
profiles:
7+
- apache
8+
ports:
9+
- "5432:5432"
10+
environment:
11+
POSTGRES_PASSWORD: R#!kro#GP43ra8Ae
12+
POSTGRES_USER: timescale_admin
13+
POSTGRES_DB: cmdscale-ef-timescaledb
14+
volumes:
15+
- cmdscale_ef_timescaledb_apache_data_volume:/var/lib/postgresql/data
16+
17+
volumes:
18+
cmdscale_ef_timescaledb_apache_data_volume:

‎docker-compose.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.9'
2-
31
services:
42
db:
53
image: timescale/timescaledb:latest-pg17

‎docs/05-apache-edition.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Apache 2 (OSS) Edition Support
2+
3+
TimescaleDB ships in two editions: the community edition (the default `timescale/timescaledb` images) and the Apache 2 edition (the `-oss` image tags). The Apache edition omits every community-only capability See the [TimescaleDB editions reference](https://www.tigerdata.com/docs/about/latest/timescaledb-editions) for the upstream feature split.
4+
5+
The target edition is a provider option. By default the provider targets the community edition. To target an Apache-edition server, opt in explicitly:
6+
7+
```csharp
8+
optionsBuilder.UseNpgsql(connectionString)
9+
.UseTimescaleDb(o => o.UseApacheEdition());
10+
```
11+
12+
With `UseApacheEdition()`, community-only statements are omitted from generated migration SQL. Each omitted feature leaves a `-- Skipping Community Edition feature (<feature>) - not available in Apache Edition` comment in the SQL (visible in `dotnet ef migrations script` output) and raises a warning through the EF Core logger while the SQL is generated.
13+
14+
Migration SQL is produced at apply/script time from the operations stored in migration files, not at `dotnet ef migrations add` time. Toggling `UseApacheEdition()` therefore changes the SQL of existing migrations without regenerating them.
15+
16+
> :warning: **Note:** Omitted features cause the model and the database to diverge on Apache servers. The configured model still carries the feature configuration; it simply is not applied to the database. The skip comment and the generation-time warning are the only signals.
17+
18+
## Feature support with `UseApacheEdition()`
19+
20+
| Feature | Behavior |
21+
| --- | --- |
22+
| Hypertables | Fully supported |
23+
| Columnstore / compression settings | Omitted with skip comment and warning |
24+
| Chunk skipping | Omitted with skip comment and warning |
25+
| Compression (columnstore) policy | Omitted with skip comment and warning |
26+
| Retention policy | Omitted with skip comment and warning |
27+
| Reorder policy | Omitted with skip comment and warning |
28+
| Continuous aggregates | Omitted with skip comment and warning |
29+
| Continuous aggregate refresh policy | Omitted with skip comment and warning |
30+
| Scaffolding (`dotnet ef dbcontext scaffold`) | Fully supported |
31+
32+
Scaffolding reads only catalog views that exist in both editions, so scaffolding an Apache-edition database produces a valid model.
33+
34+
## Edition mismatch
35+
36+
The option describes the target server; the provider does not probe the server's license at runtime.
37+
38+
- **Default (community) SQL against an Apache server:** the first community-only statement fails the migration with `functionality not supported under the current "apache" license`. Switch the context to `UseApacheEdition()`.
39+
- **`UseApacheEdition()` SQL against a community server:** the migration succeeds, but every community-only feature in the model is silently absent from the database. Remove the option to apply the full model.

‎src/Eftdb/Generators/CompressionPolicySqlGenerator.cs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
99
/// </summary>
1010
internal static class CompressionPolicySqlGenerator
1111
{
12-
public static List<string> Generate(AddCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
12+
private const string CommunityWarning = "Skipping Community Edition feature (compression policy) - not available in Apache Edition";
13+
14+
public static List<string> Generate(AddCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
1315
{
1416
List<string> statements =
1517
[
@@ -25,10 +27,10 @@ public static List<string> Generate(AddCompressionPolicyOperation operation, boo
2527
useLegacyCompressionNames)
2628
];
2729

28-
return statements;
30+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
2931
}
3032

31-
public static List<string> Generate(AlterCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
33+
public static List<string> Generate(AlterCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
3234
{
3335
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
3436

@@ -47,18 +49,18 @@ public static List<string> Generate(AlterCompressionPolicyOperation operation, b
4749
useLegacyCompressionNames)
4850
];
4951

50-
return statements;
52+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
5153
}
5254

53-
public static List<string> Generate(DropCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
55+
public static List<string> Generate(DropCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
5456
{
5557
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
5658

5759
List<string> statements =
5860
[
5961
BuildRemovePolicySql(qualifiedTableName, useLegacyCompressionNames)
6062
];
61-
return statements;
63+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
6264
}
6365

6466
private static string BuildRemovePolicySql(string qualifiedTableName, bool useLegacy)

‎src/Eftdb/Generators/CompressionSettingsSqlHelper.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ internal static string OrderByOptionName(bool useLegacy)
4646
=> useLegacy ? "timescaledb.compress_orderby" : "timescaledb.orderby";
4747

4848
/// <summary>
49-
/// Appends a community-feature-guarded compression statement to <paramref name="statements"/>
50-
/// when compression is configured for a newly created relation (hypertable or materialized view).
49+
/// Appends the compression SET statement to <paramref name="statements"/> when compression
50+
/// is configured for a newly created relation (hypertable or materialized view).
5151
/// </summary>
5252
/// <param name="statements">The statement list to append to.</param>
5353
/// <param name="relationName">The unqualified relation name.</param>
@@ -56,7 +56,6 @@ internal static string OrderByOptionName(bool useLegacy)
5656
/// <param name="compressionSegmentBy">Segment-by column list, or <see langword="null"/>.</param>
5757
/// <param name="compressionOrderBy">Order-by column list, or <see langword="null"/>.</param>
5858
/// <param name="alterDdl">The DDL keyword phrase used to target the relation (e.g., <c>"ALTER TABLE"</c> or <c>"ALTER MATERIALIZED VIEW"</c>).</param>
59-
/// <param name="warningText">The RAISE WARNING text for the Apache Edition path.</param>
6059
/// <param name="useLegacy">When <see langword="true"/>, emits pre-2.18 compression option names.</param>
6160
internal static void AppendCreateCompressionStatements(
6261
List<string> statements,
@@ -66,7 +65,6 @@ internal static void AppendCreateCompressionStatements(
6665
IReadOnlyList<string>? compressionSegmentBy,
6766
IReadOnlyList<string>? compressionOrderBy,
6867
string alterDdl,
69-
string warningText,
7068
bool useLegacy = false)
7169
{
7270
bool hasSegmentBy = compressionSegmentBy is { Count: > 0 };
@@ -95,7 +93,7 @@ internal static void AppendCreateCompressionStatements(
9593

9694
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(relationName, schema);
9795
string setClause = $"{alterDdl} {qualifiedIdentifier} SET ({string.Join(", ", compressionSettings)});";
98-
statements.Add(SqlBuilderHelper.WrapCommunityFeatures([setClause], warningText));
96+
statements.Add(setClause);
9997
}
10098

10199
/// <summary>

‎src/Eftdb/Generators/ContinuousAggregatePolicySqlGenerator.cs‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
55
/// <summary>
66
/// Generates SQL for continuous aggregate refresh policy operations.
77
/// </summary>
8-
internal class ContinuousAggregatePolicySqlGenerator
8+
internal static class ContinuousAggregatePolicySqlGenerator
99
{
10+
private const string CommunityWarning = "Skipping Community Edition feature (continuous aggregate policy) - not available in Apache Edition";
11+
1012
/// <summary>
1113
/// Generates SQL statements for adding a continuous aggregate refresh policy.
1214
/// </summary>
1315
/// <param name="operation">The add policy operation.</param>
1416
/// <returns>A list of SQL statements to execute.</returns>
15-
public static List<string> Generate(AddContinuousAggregatePolicyOperation operation)
17+
public static List<string> Generate(AddContinuousAggregatePolicyOperation operation, bool isApacheEdition = false)
1618
{
1719
string qualifiedViewName = SqlBuilderHelper.Regclass(operation.MaterializedViewName, operation.Schema);
1820

@@ -79,15 +81,15 @@ public static List<string> Generate(AddContinuousAggregatePolicyOperation operat
7981

8082
string sql = $"SELECT add_continuous_aggregate_policy({string.Join(", ", arguments)});";
8183

82-
return [sql];
84+
return SqlBuilderHelper.SkipOnApacheEdition([sql], CommunityWarning, isApacheEdition);
8385
}
8486

8587
/// <summary>
8688
/// Generates SQL statements for removing a continuous aggregate refresh policy.
8789
/// </summary>
8890
/// <param name="operation">The remove policy operation.</param>
8991
/// <returns>A list of SQL statements to execute.</returns>
90-
public static List<string> Generate(RemoveContinuousAggregatePolicyOperation operation)
92+
public static List<string> Generate(RemoveContinuousAggregatePolicyOperation operation, bool isApacheEdition = false)
9193
{
9294
string qualifiedViewName = SqlBuilderHelper.Regclass(operation.MaterializedViewName, operation.Schema);
9395

@@ -100,7 +102,7 @@ public static List<string> Generate(RemoveContinuousAggregatePolicyOperation ope
100102

101103
string sql = $"SELECT remove_continuous_aggregate_policy({string.Join(", ", arguments)});";
102104

103-
return [sql];
105+
return SqlBuilderHelper.SkipOnApacheEdition([sql], CommunityWarning, isApacheEdition);
104106
}
105107
}
106108
}

‎src/Eftdb/Generators/ContinuousAggregateSqlGenerator.cs‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33

44
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
55
{
6-
internal class ContinuousAggregateSqlGenerator
6+
internal static class ContinuousAggregateSqlGenerator
77
{
8-
private const string CommunityWarning = "Skipping Community Edition features (compression) - not available in Apache Edition";
8+
private const string CaggCommunityWarning = "Skipping Community Edition feature (continuous aggregate) - not available in Apache Edition";
99
private const string AlterDdl = "ALTER MATERIALIZED VIEW";
1010

11-
public static List<string> Generate(CreateContinuousAggregateOperation operation, bool useLegacyCompressionNames = false)
11+
public static List<string> Generate(CreateContinuousAggregateOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
1212
{
13+
if (isApacheEdition)
14+
{
15+
return [SqlBuilderHelper.SkipComment(CaggCommunityWarning)];
16+
}
17+
1318
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.MaterializedViewName, operation.Schema);
1419

1520
List<string> statements = [];
@@ -67,7 +72,6 @@ private static List<string> GenerateFromRawViewDefinition(
6772
operation.CompressionSegmentBy,
6873
operation.CompressionOrderBy,
6974
AlterDdl,
70-
CommunityWarning,
7175
useLegacyCompressionNames);
7276

7377
return statements;
@@ -191,13 +195,12 @@ private static List<string> GenerateFromStructuredQuery(
191195
operation.CompressionSegmentBy,
192196
operation.CompressionOrderBy,
193197
AlterDdl,
194-
CommunityWarning,
195198
useLegacyCompressionNames);
196199

197200
return statements;
198201
}
199202

200-
public static List<string> Generate(AlterContinuousAggregateOperation operation, bool useLegacyCompressionNames = false)
203+
public static List<string> Generate(AlterContinuousAggregateOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
201204
{
202205
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.MaterializedViewName, operation.Schema);
203206
List<string> statements = [];
@@ -248,10 +251,10 @@ public static List<string> Generate(AlterContinuousAggregateOperation operation,
248251
if (compressionSettings.Count > 0)
249252
{
250253
string setClause = $"ALTER MATERIALIZED VIEW {qualifiedIdentifier} SET ({string.Join(", ", compressionSettings)});";
251-
statements.Add(SqlBuilderHelper.WrapCommunityFeatures([setClause], CommunityWarning));
254+
statements.Add(setClause);
252255
}
253256

254-
return statements;
257+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CaggCommunityWarning, isApacheEdition);
255258
}
256259

257260
public static List<string> Generate(DropContinuousAggregateOperation operation)

‎src/Eftdb/Generators/HypertableSqlGenerator.cs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
77
{
8-
internal class HypertableSqlGenerator
8+
internal static class HypertableSqlGenerator
99
{
1010
private const string CommunityWarning = "Skipping Community Edition features (compression, chunk skipping) - not available in Apache Edition";
1111

12-
public static List<string> Generate(CreateHypertableOperation operation, bool useLegacyCompressionNames = false)
12+
public static List<string> Generate(CreateHypertableOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
1313
{
1414
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
1515
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
@@ -96,14 +96,11 @@ public static List<string> Generate(CreateHypertableOperation operation, bool us
9696
}
9797
}
9898

99-
if (communityStatements.Count > 0)
100-
{
101-
statements.Add(SqlBuilderHelper.WrapCommunityFeatures(communityStatements, CommunityWarning));
102-
}
99+
statements.AddRange(SqlBuilderHelper.SkipOnApacheEdition(communityStatements, CommunityWarning, isApacheEdition));
103100
return statements;
104101
}
105102

106-
public static List<string> Generate(AlterHypertableOperation operation, bool useLegacyCompressionNames = false)
103+
public static List<string> Generate(AlterHypertableOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
107104
{
108105
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
109106
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
@@ -126,10 +123,7 @@ public static List<string> Generate(AlterHypertableOperation operation, bool use
126123
ApplyChunkSkippingChanges(operation, qualifiedTableName, communityStatements);
127124
ApplyDimensionChanges(operation, qualifiedTableName, statements);
128125

129-
if (communityStatements.Count > 0)
130-
{
131-
statements.Add(SqlBuilderHelper.WrapCommunityFeatures(communityStatements, CommunityWarning));
132-
}
126+
statements.AddRange(SqlBuilderHelper.SkipOnApacheEdition(communityStatements, CommunityWarning, isApacheEdition));
133127
return statements;
134128
}
135129

‎src/Eftdb/Generators/ReorderPolicySqlGenerator.cs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
44
{
5-
internal class ReorderPolicySqlGenerator
5+
internal static class ReorderPolicySqlGenerator
66
{
77
private const string ProcName = "policy_reorder";
8+
private const string CommunityWarning = "Skipping Community Edition feature (reorder policy) - not available in Apache Edition";
89

9-
public static List<string> Generate(AddReorderPolicyOperation operation)
10+
public static List<string> Generate(AddReorderPolicyOperation operation, bool isApacheEdition = false)
1011
{
1112
List<string> statements =
1213
[
@@ -20,10 +21,10 @@ public static List<string> Generate(AddReorderPolicyOperation operation)
2021
statements.Add(PolicyJobSqlBuilder.BuildAlterJobSql(operation.TableName, operation.Schema, ProcName, jobClauses));
2122
}
2223

23-
return statements;
24+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
2425
}
2526

26-
public static List<string> Generate(AlterReorderPolicyOperation operation)
27+
public static List<string> Generate(AlterReorderPolicyOperation operation, bool isApacheEdition = false)
2728
{
2829
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
2930

@@ -56,18 +57,18 @@ public static List<string> Generate(AlterReorderPolicyOperation operation)
5657
}
5758
}
5859

59-
return statements;
60+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
6061
}
6162

62-
public static List<string> Generate(DropReorderPolicyOperation operation)
63+
public static List<string> Generate(DropReorderPolicyOperation operation, bool isApacheEdition = false)
6364
{
6465
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
6566

6667
List<string> statements =
6768
[
6869
$"SELECT remove_reorder_policy({qualifiedTableName}, if_exists => true);"
6970
];
70-
return statements;
71+
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
7172
}
7273

7374
private static string BuildAddReorderPolicySql(string tableName, string schema, string indexName, DateTime? initialStart)

0 commit comments

Comments
 (0)