Skip to content

Commit b81eb34

Browse files
fix: deliver TimescaleDB warnings to LogTo sinks and make them configurable
1 parent f07e811 commit b81eb34

12 files changed

Lines changed: 562 additions & 16 deletions

‎.claude/reference/architecture.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ src/Eftdb.Design/
3939

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

42-
- **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.
42+
- **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.
4343
- **Design public surface** = only the pipeline entry types (`TimescaleDBDesignTimeServices`, `TimescaleDatabaseModelFactory`, `TimescaleDbCodeGenerator`, `TimescaleCSharpMigrationOperationGenerator`, and the `Generators/Timescale*` classes). All per-feature Design types are internal.
4444

4545
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>`.
@@ -69,6 +69,7 @@ Runtime (`src/Eftdb/`):
6969
- `Generators/PolicyJobSqlBuilder` — shared `alter_job` clause builder
7070
- `Generators/CompressionSettingsSqlHelper` — `SET (timescaledb.enable_columnstore = ...)` vs legacy `timescaledb.compress` clause, changed-settings diff
7171
- `Query/` — `EF.Functions.TimeBucket()` overloads + `Internal/` translator plugin mapping to `time_bucket(...)`; runtime-only, throw outside LINQ
72+
- `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).
7273

7374
Design (`src/Eftdb.Design/`):
7475
- `TimescaleDBDesignTimeServices` — registers `TimescaleCSharpMigrationOperationGenerator`, `TimescaleDatabaseModelFactory`, `TimescaleDbAnnotationCodeGenerator`, `TimescaleModelCodeGeneratorSelector`

‎docs/05-apache-edition.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ optionsBuilder.UseNpgsql(connectionString)
99
.UseTimescaleDb(o => o.UseApacheEdition());
1010
```
1111

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.
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. The warning is raised as `TimescaleDbEventId.CommunityFeatureSkipped` and can be suppressed with `ConfigureWarnings(w => w.Ignore(...))` or escalated to an exception with `w.Throw(...)`.
1313

1414
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.
1515

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

3838
- **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()`.
3939
- **`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.
40+
41+
## Diagnostics event IDs
42+
43+
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`).
44+
45+
| Event ID | Value | Meaning |
46+
| --- | --- | --- |
47+
| `CommunityFeatureSkipped` | 63000 | A Community-only feature (compression, policy, or continuous aggregate) was skipped at migration SQL generation because `UseApacheEdition()` is set. |
48+
| `TimeBucketColumnUnmapped` | 63001 | A continuous aggregate exposes a `time_bucket` column that no property maps to. Raised at model validation. |
49+
50+
```csharp
51+
using CmdScale.EntityFrameworkCore.TimescaleDB.Diagnostics;
52+
53+
optionsBuilder.UseNpgsql(connectionString)
54+
.UseTimescaleDb()
55+
.ConfigureWarnings(w => w
56+
.Ignore(TimescaleDbEventId.CommunityFeatureSkipped)
57+
.Throw(TimescaleDbEventId.TimeBucketColumnUnmapped));
58+
```

‎docs/data-annotations/continuous-aggregates.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ Structured aggregates (those configured through attributes rather than a raw vie
159159

160160
- 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.
161161
- 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).
162-
- 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.
162+
- 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(...)`.
163163

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

‎docs/fluent-api/continuous-aggregates.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ Structured aggregates (those configured through the builders rather than a raw v
203203

204204
- 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.
205205
- A property designated via `.WithTimeBucketProperty(...)` that does not exist on the entity raises an `InvalidOperationException`.
206-
- 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.
206+
- 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(...)`.
207207

208208
> :warning: **Note:** Entities scaffolded with a raw view definition are exempt from all checks, because the structured projection fields are unused on that path.
209209
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.Extensions.Logging;
3+
4+
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Diagnostics
5+
{
6+
/// <summary>
7+
/// Event IDs for TimescaleDB provider events that are logged to an <see cref="ILogger"/> and dispatched
8+
/// through Entity Framework Core's diagnostics pipeline (ILogger sinks, <c>LogTo</c>, and
9+
/// <see cref="System.Diagnostics.DiagnosticSource"/>).
10+
/// </summary>
11+
/// <remarks>
12+
/// These IDs are also used with <see cref="Microsoft.EntityFrameworkCore.Diagnostics.WarningsConfigurationBuilder"/>
13+
/// (via <c>ConfigureWarnings</c>) to silence, elevate, or throw on a given warning, e.g.
14+
/// <c>options.ConfigureWarnings(w =&gt; w.Ignore(TimescaleDbEventId.CommunityFeatureSkipped))</c>.
15+
/// <para>
16+
/// The numeric base is <c>63000</c>. EF Core reserves the 10000s (<c>CoreEventId</c>), EF Relational the
17+
/// 20000s (<c>RelationalEventId</c>), and the Npgsql provider the 35000s (<c>NpgsqlEfEventId</c>).
18+
/// </para>
19+
/// </remarks>
20+
public static class TimescaleDbEventId
21+
{
22+
// Base chosen to clear EF Core (10000s), EF Relational (20000s), and Npgsql (35000s) ranges.
23+
private const int Base = 63000;
24+
25+
private enum Id
26+
{
27+
CommunityFeatureSkipped = Base,
28+
TimeBucketColumnUnmapped,
29+
}
30+
31+
/// <summary>
32+
/// A Community/TSL-only TimescaleDB feature was skipped because the provider is running in Apache
33+
/// edition mode (<c>UseApacheEdition()</c>). Raised at migration SQL generation time, once per
34+
/// skipped feature, in the <see cref="DbLoggerCategory.Migrations"/> category.
35+
/// </summary>
36+
public static readonly EventId CommunityFeatureSkipped = MakeMigrationsId(Id.CommunityFeatureSkipped);
37+
38+
/// <summary>
39+
/// A continuous aggregate exposes its time-bucket column but no property maps to that column, so the
40+
/// bucket cannot be queried through the entity. Raised at model validation time in the
41+
/// <see cref="DbLoggerCategory.Model.Validation"/> category.
42+
/// </summary>
43+
public static readonly EventId TimeBucketColumnUnmapped = MakeValidationId(Id.TimeBucketColumnUnmapped);
44+
45+
private static readonly string MigrationsPrefix = DbLoggerCategory.Migrations.Name + ".";
46+
private static readonly string ValidationPrefix = DbLoggerCategory.Model.Validation.Name + ".";
47+
48+
private static EventId MakeMigrationsId(Id id) => new((int)id, MigrationsPrefix + id);
49+
50+
private static EventId MakeValidationId(Id id) => new((int)id, ValidationPrefix + id);
51+
}
52+
}

0 commit comments

Comments
 (0)