Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ For convenient local development, a `docker-compose.yml` file is included in the

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

#### Switching between TimescaleDB editions

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.

The auto-loaded `docker-compose.override.yml` adds a `db-apache` service for this. Both editions bind the same port `5432` with identical credentials.

> [!WARNING]
> Each edition has its own data volume, and they are **not interchangeable**

### 🧪 Testing

This project uses a two-tier testing strategy to ensure code quality and correctness.
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
db-apache:
image: timescale/timescaledb:latest-pg17-oss
container_name: cmdscale-ef-timescaledb-apache
restart: always
profiles:
- apache
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: R#!kro#GP43ra8Ae
POSTGRES_USER: timescale_admin
POSTGRES_DB: cmdscale-ef-timescaledb
volumes:
- cmdscale_ef_timescaledb_apache_data_volume:/var/lib/postgresql/data

volumes:
cmdscale_ef_timescaledb_apache_data_volume:
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.9'

services:
db:
image: timescale/timescaledb:latest-pg17
Expand Down
39 changes: 39 additions & 0 deletions docs/05-apache-edition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Apache 2 (OSS) Edition Support

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.

The target edition is a provider option. By default the provider targets the community edition. To target an Apache-edition server, opt in explicitly:

```csharp
optionsBuilder.UseNpgsql(connectionString)
.UseTimescaleDb(o => o.UseApacheEdition());
```

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.

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.

> :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.

## Feature support with `UseApacheEdition()`

| Feature | Behavior |
| --- | --- |
| Hypertables | Fully supported |
| Columnstore / compression settings | Omitted with skip comment and warning |
| Chunk skipping | Omitted with skip comment and warning |
| Compression (columnstore) policy | Omitted with skip comment and warning |
| Retention policy | Omitted with skip comment and warning |
| Reorder policy | Omitted with skip comment and warning |
| Continuous aggregates | Omitted with skip comment and warning |
| Continuous aggregate refresh policy | Omitted with skip comment and warning |
| Scaffolding (`dotnet ef dbcontext scaffold`) | Fully supported |

Scaffolding reads only catalog views that exist in both editions, so scaffolding an Apache-edition database produces a valid model.

## Edition mismatch

The option describes the target server; the provider does not probe the server's license at runtime.

- **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()`.
- **`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.
14 changes: 8 additions & 6 deletions src/Eftdb/Generators/CompressionPolicySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
/// </summary>
internal static class CompressionPolicySqlGenerator
{
public static List<string> Generate(AddCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
private const string CommunityWarning = "Skipping Community Edition feature (compression policy) - not available in Apache Edition";

public static List<string> Generate(AddCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
List<string> statements =
[
Expand All @@ -25,10 +27,10 @@ public static List<string> Generate(AddCompressionPolicyOperation operation, boo
useLegacyCompressionNames)
];

return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

public static List<string> Generate(AlterCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(AlterCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);

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

return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

public static List<string> Generate(DropCompressionPolicyOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(DropCompressionPolicyOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);

List<string> statements =
[
BuildRemovePolicySql(qualifiedTableName, useLegacyCompressionNames)
];
return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

private static string BuildRemovePolicySql(string qualifiedTableName, bool useLegacy)
Expand Down
8 changes: 3 additions & 5 deletions src/Eftdb/Generators/CompressionSettingsSqlHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ internal static string OrderByOptionName(bool useLegacy)
=> useLegacy ? "timescaledb.compress_orderby" : "timescaledb.orderby";

/// <summary>
/// Appends a community-feature-guarded compression statement to <paramref name="statements"/>
/// when compression is configured for a newly created relation (hypertable or materialized view).
/// Appends the compression SET statement to <paramref name="statements"/> when compression
/// is configured for a newly created relation (hypertable or materialized view).
/// </summary>
/// <param name="statements">The statement list to append to.</param>
/// <param name="relationName">The unqualified relation name.</param>
Expand All @@ -56,7 +56,6 @@ internal static string OrderByOptionName(bool useLegacy)
/// <param name="compressionSegmentBy">Segment-by column list, or <see langword="null"/>.</param>
/// <param name="compressionOrderBy">Order-by column list, or <see langword="null"/>.</param>
/// <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>
/// <param name="warningText">The RAISE WARNING text for the Apache Edition path.</param>
/// <param name="useLegacy">When <see langword="true"/>, emits pre-2.18 compression option names.</param>
internal static void AppendCreateCompressionStatements(
List<string> statements,
Expand All @@ -66,7 +65,6 @@ internal static void AppendCreateCompressionStatements(
IReadOnlyList<string>? compressionSegmentBy,
IReadOnlyList<string>? compressionOrderBy,
string alterDdl,
string warningText,
bool useLegacy = false)
{
bool hasSegmentBy = compressionSegmentBy is { Count: > 0 };
Expand Down Expand Up @@ -95,7 +93,7 @@ internal static void AppendCreateCompressionStatements(

string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(relationName, schema);
string setClause = $"{alterDdl} {qualifiedIdentifier} SET ({string.Join(", ", compressionSettings)});";
statements.Add(SqlBuilderHelper.WrapCommunityFeatures([setClause], warningText));
statements.Add(setClause);
}

/// <summary>
Expand Down
12 changes: 7 additions & 5 deletions src/Eftdb/Generators/ContinuousAggregatePolicySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators
/// <summary>
/// Generates SQL for continuous aggregate refresh policy operations.
/// </summary>
internal class ContinuousAggregatePolicySqlGenerator
internal static class ContinuousAggregatePolicySqlGenerator
{
private const string CommunityWarning = "Skipping Community Edition feature (continuous aggregate policy) - not available in Apache Edition";

/// <summary>
/// Generates SQL statements for adding a continuous aggregate refresh policy.
/// </summary>
/// <param name="operation">The add policy operation.</param>
/// <returns>A list of SQL statements to execute.</returns>
public static List<string> Generate(AddContinuousAggregatePolicyOperation operation)
public static List<string> Generate(AddContinuousAggregatePolicyOperation operation, bool isApacheEdition = false)
{
string qualifiedViewName = SqlBuilderHelper.Regclass(operation.MaterializedViewName, operation.Schema);

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

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

return [sql];
return SqlBuilderHelper.SkipOnApacheEdition([sql], CommunityWarning, isApacheEdition);
}

/// <summary>
/// Generates SQL statements for removing a continuous aggregate refresh policy.
/// </summary>
/// <param name="operation">The remove policy operation.</param>
/// <returns>A list of SQL statements to execute.</returns>
public static List<string> Generate(RemoveContinuousAggregatePolicyOperation operation)
public static List<string> Generate(RemoveContinuousAggregatePolicyOperation operation, bool isApacheEdition = false)
{
string qualifiedViewName = SqlBuilderHelper.Regclass(operation.MaterializedViewName, operation.Schema);

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

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

return [sql];
return SqlBuilderHelper.SkipOnApacheEdition([sql], CommunityWarning, isApacheEdition);
}
}
}
19 changes: 11 additions & 8 deletions src/Eftdb/Generators/ContinuousAggregateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@

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

public static List<string> Generate(CreateContinuousAggregateOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(CreateContinuousAggregateOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
if (isApacheEdition)
{
return [SqlBuilderHelper.SkipComment(CaggCommunityWarning)];
}

string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.MaterializedViewName, operation.Schema);

List<string> statements = [];
Expand Down Expand Up @@ -67,7 +72,6 @@ private static List<string> GenerateFromRawViewDefinition(
operation.CompressionSegmentBy,
operation.CompressionOrderBy,
AlterDdl,
CommunityWarning,
useLegacyCompressionNames);

return statements;
Expand Down Expand Up @@ -191,13 +195,12 @@ private static List<string> GenerateFromStructuredQuery(
operation.CompressionSegmentBy,
operation.CompressionOrderBy,
AlterDdl,
CommunityWarning,
useLegacyCompressionNames);

return statements;
}

public static List<string> Generate(AlterContinuousAggregateOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(AlterContinuousAggregateOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.MaterializedViewName, operation.Schema);
List<string> statements = [];
Expand Down Expand Up @@ -248,10 +251,10 @@ public static List<string> Generate(AlterContinuousAggregateOperation operation,
if (compressionSettings.Count > 0)
{
string setClause = $"ALTER MATERIALIZED VIEW {qualifiedIdentifier} SET ({string.Join(", ", compressionSettings)});";
statements.Add(SqlBuilderHelper.WrapCommunityFeatures([setClause], CommunityWarning));
statements.Add(setClause);
}

return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CaggCommunityWarning, isApacheEdition);
}

public static List<string> Generate(DropContinuousAggregateOperation operation)
Expand Down
16 changes: 5 additions & 11 deletions src/Eftdb/Generators/HypertableSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

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

public static List<string> Generate(CreateHypertableOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(CreateHypertableOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
Expand Down Expand Up @@ -96,14 +96,11 @@ public static List<string> Generate(CreateHypertableOperation operation, bool us
}
}

if (communityStatements.Count > 0)
{
statements.Add(SqlBuilderHelper.WrapCommunityFeatures(communityStatements, CommunityWarning));
}
statements.AddRange(SqlBuilderHelper.SkipOnApacheEdition(communityStatements, CommunityWarning, isApacheEdition));
return statements;
}

public static List<string> Generate(AlterHypertableOperation operation, bool useLegacyCompressionNames = false)
public static List<string> Generate(AlterHypertableOperation operation, bool useLegacyCompressionNames = false, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);
string qualifiedIdentifier = SqlBuilderHelper.QualifiedIdentifier(operation.TableName, operation.Schema);
Expand All @@ -126,10 +123,7 @@ public static List<string> Generate(AlterHypertableOperation operation, bool use
ApplyChunkSkippingChanges(operation, qualifiedTableName, communityStatements);
ApplyDimensionChanges(operation, qualifiedTableName, statements);

if (communityStatements.Count > 0)
{
statements.Add(SqlBuilderHelper.WrapCommunityFeatures(communityStatements, CommunityWarning));
}
statements.AddRange(SqlBuilderHelper.SkipOnApacheEdition(communityStatements, CommunityWarning, isApacheEdition));
return statements;
}

Expand Down
15 changes: 8 additions & 7 deletions src/Eftdb/Generators/ReorderPolicySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

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

public static List<string> Generate(AddReorderPolicyOperation operation)
public static List<string> Generate(AddReorderPolicyOperation operation, bool isApacheEdition = false)
{
List<string> statements =
[
Expand All @@ -20,10 +21,10 @@ public static List<string> Generate(AddReorderPolicyOperation operation)
statements.Add(PolicyJobSqlBuilder.BuildAlterJobSql(operation.TableName, operation.Schema, ProcName, jobClauses));
}

return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

public static List<string> Generate(AlterReorderPolicyOperation operation)
public static List<string> Generate(AlterReorderPolicyOperation operation, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);

Expand Down Expand Up @@ -56,18 +57,18 @@ public static List<string> Generate(AlterReorderPolicyOperation operation)
}
}

return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

public static List<string> Generate(DropReorderPolicyOperation operation)
public static List<string> Generate(DropReorderPolicyOperation operation, bool isApacheEdition = false)
{
string qualifiedTableName = SqlBuilderHelper.Regclass(operation.TableName, operation.Schema);

List<string> statements =
[
$"SELECT remove_reorder_policy({qualifiedTableName}, if_exists => true);"
];
return statements;
return SqlBuilderHelper.SkipOnApacheEdition(statements, CommunityWarning, isApacheEdition);
}

private static string BuildAddReorderPolicySql(string tableName, string schema, string indexName, DateTime? initialStart)
Expand Down
Loading
Loading