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
3 changes: 2 additions & 1 deletion .claude/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ src/Eftdb.Design/

Implementation types are `internal` in both packages (tests reach them via `InternalsVisibleTo`); keep new ones internal:

- **Runtime public surface** = the consumer contract only: attributes, type builders + string builders, `OrderBy*`/`SparseIndex*` fluent types, `Abstractions/`, `EF.Functions` extensions, bulk copy, `UseTimescaleDb()`/`TimescaleDbOptions`, `MigrationExtensions`, `Operations` (appear in `OperationBuilder<T>` signatures), plus `{Feature}Annotations` and `DefaultValues` (kept public so consumers can read config off a built model). Differs, model extractors, SQL generators, conventions, `SqlBuilderHelper`, `PolicyJobSqlBuilder`, and the `Timescale*` differ/SQL-generator/convention-plugin classes are internal.
- **Runtime public surface** = the consumer contract only: attributes, type builders + string builders, `OrderBy*`/`SparseIndex*` fluent types, `Abstractions/`, `EF.Functions` extensions, bulk copy, `UseTimescaleDb()`/`TimescaleDbOptions`, `MigrationExtensions`, `Operations` (appear in `OperationBuilder<T>` signatures), plus `{Feature}Annotations` and `DefaultValues` (kept public so consumers can read config off a built model), and `Diagnostics/TimescaleDbEventId` + `Diagnostics/TimescaleDbLoggingDefinitions` (stable event IDs for `ConfigureWarnings`; definitions class must be public for the `LoggingDefinitions` service replacement). Differs, model extractors, SQL generators, conventions, `SqlBuilderHelper`, `PolicyJobSqlBuilder`, `Diagnostics/TimescaleDbLoggerExtensions`, and the `Timescale*` differ/SQL-generator/convention-plugin classes are internal.
- **Design public surface** = only the pipeline entry types (`TimescaleDBDesignTimeServices`, `TimescaleDatabaseModelFactory`, `TimescaleDbCodeGenerator`, `TimescaleCSharpMigrationOperationGenerator`, and the `Generators/Timescale*` classes). All per-feature Design types are internal.

Hypertable extras: `DimensionAttribute`, `SparseIndex` + `SparseIndexAttribute` + `SparseIndexValidationConvention` (validates bloom/minmax arity, segmentby/orderby prerequisites, duplicates at model finalization). ContinuousAggregate extras: property-level `TimeBucketAttribute`, `AggregateAttribute`, `GroupByColumnAttribute`; generic `ContinuousAggregateBuilder<TEntity, TSource>`.
Expand Down Expand Up @@ -69,6 +69,7 @@ Runtime (`src/Eftdb/`):
- `Generators/PolicyJobSqlBuilder` — shared `alter_job` clause builder
- `Generators/CompressionSettingsSqlHelper` — `SET (timescaledb.enable_columnstore = ...)` vs legacy `timescaledb.compress` clause, changed-settings diff
- `Query/` — `EF.Functions.TimeBucket()` overloads + `Internal/` translator plugin mapping to `time_bucket(...)`; runtime-only, throw outside LINQ
- `Diagnostics/` — **single place that defines and emits provider warnings**, mirroring EF's own layering: `TimescaleDbEventId` (public) exposes stable `EventId`s (base `63000`, clear of EF/relational/Npgsql ranges) usable with `ConfigureWarnings`; `TimescaleDbLoggingDefinitions : NpgsqlLoggingDefinitions` (public, replaces the `LoggingDefinitions` service via the same replace-and-derive pattern as the validator/SQL generator) holds one cached `EventDefinitionBase` field per event; `TimescaleDbLoggerExtensions` (internal) provides one strongly-typed emitter per event, lazy-initializing definitions from `logger.Definitions` (fail-loud cast — a foreign instance means the Replace registration was lost) and dispatching through EF's pipeline (ShouldLog+Log for `ILogger`, NeedsEventData+DispatchEventData for `LogTo`/`DiagnosticSource`) so warnings reach every sink and honour Ignore/Throw. New warning = EventId + definitions field + emitter method. Never emit raw `Logger.LogWarning` — `LogTo` sinks never receive it. Design-time scaffolding warnings use `IOperationReporter` instead (correct channel for the dotnet-ef console).

Design (`src/Eftdb.Design/`):
- `TimescaleDBDesignTimeServices` — registers `TimescaleCSharpMigrationOperationGenerator`, `TimescaleDatabaseModelFactory`, `TimescaleDbAnnotationCodeGenerator`, `TimescaleModelCodeGeneratorSelector`
Expand Down
21 changes: 20 additions & 1 deletion docs/05-apache-edition.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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.
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. The warning is raised as `TimescaleDbEventId.CommunityFeatureSkipped` and can be suppressed with `ConfigureWarnings(w => w.Ignore(...))` or escalated to an exception with `w.Throw(...)`.

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.

Expand Down Expand Up @@ -37,3 +37,22 @@ The option describes the target server; the provider does not probe the server's

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

## Diagnostics event IDs

Provider warnings are dispatched through EF Core's diagnostics pipeline, so they reach both `ILoggerFactory`-based logging and `LogTo(...)` sinks, and each can be controlled per event with `ConfigureWarnings`. The stable event IDs live on `TimescaleDbEventId` (namespace `CmdScale.EntityFrameworkCore.TimescaleDB.Diagnostics`).

| Event ID | Value | Meaning |
| --- | --- | --- |
| `CommunityFeatureSkipped` | 63000 | A Community-only feature (compression, policy, or continuous aggregate) was skipped at migration SQL generation because `UseApacheEdition()` is set. |
| `TimeBucketColumnUnmapped` | 63001 | A continuous aggregate exposes a `time_bucket` column that no property maps to. Raised at model validation. |

```csharp
using CmdScale.EntityFrameworkCore.TimescaleDB.Diagnostics;

optionsBuilder.UseNpgsql(connectionString)
.UseTimescaleDb()
.ConfigureWarnings(w => w
.Ignore(TimescaleDbEventId.CommunityFeatureSkipped)
.Throw(TimescaleDbEventId.TimeBucketColumnUnmapped));
```
2 changes: 1 addition & 1 deletion docs/data-annotations/continuous-aggregates.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Structured aggregates (those configured through attributes rather than a raw vie

- Duplicate output column names are rejected with an `InvalidOperationException`. The check compares the bucket column, all `[GroupByColumn]` columns, and every `[Aggregate]` alias after resolving them to database column names. A source column that collides with the bucket column name — previously surfacing only at `migrate` time — is now caught at model build.
- A property designated by a property-level `[TimeBucket]` that does not exist on the entity raises an `InvalidOperationException` (this cannot occur through attributes alone, but the same check guards Fluent-configured models).
- An aggregate with no time-bucket designation (property-level `[TimeBucket]`) and no property mapping to the default bucket column `time_bucket` emits a warning through the configured EF logger. The view still exposes a `time_bucket` column, but it cannot be queried through the entity; previously this surfaced only at query time as an opaque Postgres "column does not exist" error. Remedy by designating a property or mapping one to `time_bucket`. This is a warning rather than an exception because deliberately not exposing the bucket is legal.
- An aggregate with no time-bucket designation (property-level `[TimeBucket]`) and no property mapping to the default bucket column `time_bucket` emits a warning through the configured EF logger. The view still exposes a `time_bucket` column, but it cannot be queried through the entity; previously this surfaced only at query time as an opaque Postgres "column does not exist" error. Remedy by designating a property or mapping one to `time_bucket`. This is a warning rather than an exception because deliberately not exposing the bucket is legal. The warning is raised as `TimescaleDbEventId.TimeBucketColumnUnmapped` and can be suppressed with `ConfigureWarnings(w => w.Ignore(...))` or escalated to an exception with `w.Throw(...)`.

> :warning: **Note:** Entities scaffolded with a raw view definition are exempt from all checks, because the structured projection fields are unused on that path.

Expand Down
2 changes: 1 addition & 1 deletion docs/fluent-api/continuous-aggregates.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Structured aggregates (those configured through the builders rather than a raw v

- Duplicate output column names are rejected with an `InvalidOperationException`. The check compares the bucket column, all GROUP BY columns, and every aggregate alias after resolving them to database column names. A source column that collides with the bucket column name is caught at model build.
- A property designated via `.WithTimeBucketProperty(...)` that does not exist on the entity raises an `InvalidOperationException`.
- An aggregate with no time-bucket designation (`.WithTimeBucketProperty(...)`) and no property mapping to the default bucket column `time_bucket` emits a warning through the configured EF logger. The view still exposes a `time_bucket` column, but it cannot be queried through the entity; previously this surfaced only at query time as an opaque Postgres "column does not exist" error. Remedy by designating a property or mapping one to `time_bucket`. This is a warning rather than an exception because deliberately not exposing the bucket is legal.
- An aggregate with no time-bucket designation (`.WithTimeBucketProperty(...)`) and no property mapping to the default bucket column `time_bucket` emits a warning through the configured EF logger. The view still exposes a `time_bucket` column, but it cannot be queried through the entity; previously this surfaced only at query time as an opaque Postgres "column does not exist" error. Remedy by designating a property or mapping one to `time_bucket`. This is a warning rather than an exception because deliberately not exposing the bucket is legal. The warning is raised as `TimescaleDbEventId.TimeBucketColumnUnmapped` and can be suppressed with `ConfigureWarnings(w => w.Ignore(...))` or escalated to an exception with `w.Throw(...)`.

> :warning: **Note:** Entities scaffolded with a raw view definition are exempt from all checks, because the structured projection fields are unused on that path.

Expand Down
52 changes: 52 additions & 0 deletions src/Eftdb/Diagnostics/TimescaleDbEventId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Diagnostics
{
/// <summary>
/// Event IDs for TimescaleDB provider events that are logged to an <see cref="ILogger"/> and dispatched
/// through Entity Framework Core's diagnostics pipeline (ILogger sinks, <c>LogTo</c>, and
/// <see cref="System.Diagnostics.DiagnosticSource"/>).
/// </summary>
/// <remarks>
/// These IDs are also used with <see cref="Microsoft.EntityFrameworkCore.Diagnostics.WarningsConfigurationBuilder"/>
/// (via <c>ConfigureWarnings</c>) to silence, elevate, or throw on a given warning, e.g.
/// <c>options.ConfigureWarnings(w =&gt; w.Ignore(TimescaleDbEventId.CommunityFeatureSkipped))</c>.
/// <para>
/// The numeric base is <c>63000</c>. EF Core reserves the 10000s (<c>CoreEventId</c>), EF Relational the
/// 20000s (<c>RelationalEventId</c>), and the Npgsql provider the 35000s (<c>NpgsqlEfEventId</c>).
/// </para>
/// </remarks>
public static class TimescaleDbEventId
{
// Base chosen to clear EF Core (10000s), EF Relational (20000s), and Npgsql (35000s) ranges.
private const int Base = 63000;

private enum Id
{
CommunityFeatureSkipped = Base,
TimeBucketColumnUnmapped,
}

/// <summary>
/// A Community/TSL-only TimescaleDB feature was skipped because the provider is running in Apache
/// edition mode (<c>UseApacheEdition()</c>). Raised at migration SQL generation time, once per
/// skipped feature, in the <see cref="DbLoggerCategory.Migrations"/> category.
/// </summary>
public static readonly EventId CommunityFeatureSkipped = MakeMigrationsId(Id.CommunityFeatureSkipped);

/// <summary>
/// A continuous aggregate exposes its time-bucket column but no property maps to that column, so the
/// bucket cannot be queried through the entity. Raised at model validation time in the
/// <see cref="DbLoggerCategory.Model.Validation"/> category.
/// </summary>
public static readonly EventId TimeBucketColumnUnmapped = MakeValidationId(Id.TimeBucketColumnUnmapped);

private static readonly string MigrationsPrefix = DbLoggerCategory.Migrations.Name + ".";
private static readonly string ValidationPrefix = DbLoggerCategory.Model.Validation.Name + ".";

private static EventId MakeMigrationsId(Id id) => new((int)id, MigrationsPrefix + id);

private static EventId MakeValidationId(Id id) => new((int)id, ValidationPrefix + id);
}
}
Loading
Loading