diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 145b68f..38ac5ff 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -110,7 +110,7 @@ New Feature → [1] eftdb-feature-initializer
| `test-writer` | Creates unit and integration tests | |
| `test-coverage-planner` | Analyzes test coverage gaps | `/coverage-plan` |
| `example-feature-generator` | Creates usage examples | |
-| `git-committer` | Formats, tests, stages, generates commit message | `/prepare-commit` |
+| `git-committer` | Formats, tests, generates commit message (does not stage) | `/prepare-commit` |
| `code-detective` | Investigates bugs, traces history | |
| `pr-code-reviewer` | Reviews PR changes against patterns | `/review` |
| `eftdb-docs-writer` | Writes and updates documentation | |
diff --git a/.claude/agents/code-detective.md b/.claude/agents/code-detective.md
index 0d328d9..9b3ddbb 100644
--- a/.claude/agents/code-detective.md
+++ b/.claude/agents/code-detective.md
@@ -1,6 +1,57 @@
---
name: code-detective
-description: Use this agent when the user needs to understand the root cause of a bug, trace when it was introduced, identify the last working commit, or analyze code behavior without making any changes. This agent is for investigation and analysis only, not for fixing bugs.\n\nExamples:\n\n\nContext: User is investigating why HypertableDiffer is not detecting chunk time interval changes.\n\nuser: "The HypertableDiffer doesn't seem to detect when I change the chunk time interval from 7 days to 1 day. Can you investigate when this broke?"\n\nassistant: "I'll use the code-detective agent to analyze the HypertableDiffer implementation and trace through the git history to find when this regression was introduced."\n\n\nThe user is asking to investigate a bug's root cause and history, which is exactly what code-detective does. Use the Agent tool to launch code-detective for deep analysis of the differ logic and git history.\n\n\n\n\nContext: User wants to understand why continuous aggregate scaffolding is generating incorrect column names.\n\nuser: "I scaffolded a continuous aggregate from the database and the column names in the generated entity don't match what's in the database. What's going on?"\n\nassistant: "Let me use the code-detective agent to trace through the scaffolding pipeline and identify where the column name resolution is going wrong."\n\n\nThis is an investigation task to understand existing behavior and identify the issue source. Code-detective should analyze ContinuousAggregateScaffoldingExtractor, AnnotationApplier, and related naming convention handling without making changes.\n\n\n\n\nContext: User is trying to understand when a feature stopped working.\n\nuser: "Reorder policies worked in version 1.2.0 but now in 1.3.0 they're not being applied during migrations. Can you find out what changed?"\n\nassistant: "I'll launch the code-detective agent to compare the git history between versions 1.2.0 and 1.3.0, focusing on ReorderPolicyDiffer and related migration code."\n\n\nThe user needs historical analysis across versions to identify a regression. Code-detective should examine git commits, diffs, and potentially GitHub issues between the two versions.\n\n\n\n\nContext: User wants to understand complex code flow before making changes.\n\nuser: "Before I add support for compression policies, I want to understand how the existing reorder policy implementation works end-to-end."\n\nassistant: "I'll use the code-detective agent to trace the complete flow of reorder policies from attribute/fluent API configuration through conventions, differs, generators, and scaffolding."\n\n\nThis is a code comprehension task requiring deep analysis of implementation patterns. Code-detective should provide a detailed walkthrough without modifying anything.\n\n
+description: |-
+ Use this agent when the user needs to understand the root cause of a bug, trace when it was introduced, identify the last working commit, or analyze code behavior without making any changes. This agent is for investigation and analysis only, not for fixing bugs.
+
+ Examples:
+
+
+ Context: User is investigating why HypertableDiffer is not detecting chunk time interval changes.
+
+ user: "The HypertableDiffer doesn't seem to detect when I change the chunk time interval from 7 days to 1 day. Can you investigate when this broke?"
+
+ assistant: "I'll use the code-detective agent to analyze the HypertableDiffer implementation and trace through the git history to find when this regression was introduced."
+
+
+ The user is asking to investigate a bug's root cause and history, which is exactly what code-detective does. Use the Agent tool to launch code-detective for deep analysis of the differ logic and git history.
+
+
+
+
+ Context: User wants to understand why continuous aggregate scaffolding is generating incorrect column names.
+
+ user: "I scaffolded a continuous aggregate from the database and the column names in the generated entity don't match what's in the database. What's going on?"
+
+ assistant: "Let me use the code-detective agent to trace through the scaffolding pipeline and identify where the column name resolution is going wrong."
+
+
+ This is an investigation task to understand existing behavior and identify the issue source. Code-detective should analyze ContinuousAggregateScaffoldingExtractor, AnnotationApplier, and related naming convention handling without making changes.
+
+
+
+
+ Context: User is trying to understand when a feature stopped working.
+
+ user: "Reorder policies worked in version 1.2.0 but now in 1.3.0 they're not being applied during migrations. Can you find out what changed?"
+
+ assistant: "I'll launch the code-detective agent to compare the git history between versions 1.2.0 and 1.3.0, focusing on ReorderPolicyDiffer and related migration code."
+
+
+ The user needs historical analysis across versions to identify a regression. Code-detective should examine git commits, diffs, and potentially GitHub issues between the two versions.
+
+
+
+
+ Context: User wants to understand complex code flow before making changes.
+
+ user: "Before I add support for compression policies, I want to understand how the existing reorder policy implementation works end-to-end."
+
+ assistant: "I'll use the code-detective agent to trace the complete flow of reorder policies from attribute/fluent API configuration through conventions, differs, generators, and scaffolding."
+
+
+ This is a code comprehension task requiring deep analysis of implementation patterns. Code-detective should provide a detailed walkthrough without modifying anything.
+
+
tools: Bash, Glob, Grep, Read, WebSearch, AskUserQuestion
model: sonnet
color: red
diff --git a/.claude/agents/eftdb-bug-fixer.md b/.claude/agents/eftdb-bug-fixer.md
index c0fe457..788bf65 100644
--- a/.claude/agents/eftdb-bug-fixer.md
+++ b/.claude/agents/eftdb-bug-fixer.md
@@ -1,6 +1,35 @@
---
name: eftdb-bug-fixer
-description: Use this agent when bugs are discovered in existing runtime or design-time code within the CmdScale.EntityFrameworkCore.TimescaleDB library. This includes:\n\n\nContext: User discovers a bug in the HypertableDiffer.\nuser: "The HypertableDiffer is not detecting changes to chunk time interval"\nassistant: "I'll use the eftdb-bug-fixer agent to analyze and fix the HypertableDiffer issue."\n\n\n\n\nContext: SQL generation is incorrect for reorder policies.\nuser: "The ReorderPolicySqlGenerator is generating invalid SQL with wrong schema qualification"\nassistant: "I'll launch the eftdb-bug-fixer agent to fix the SQL generation bug in ReorderPolicySqlGenerator."\n\n\n\n\nContext: Scaffolding extractor query is failing.\nuser: "The ContinuousAggregateScaffoldingExtractor is throwing NullReferenceException when extracting aggregate functions"\nassistant: "Let me use the eftdb-bug-fixer agent to debug and fix the scaffolding extractor."\n\n\n\n\nContext: Another agent reports a bug during its work.\nuser: "The eftdb-scaffold-support agent reported a mismatch between runtime annotations and scaffolding expectations"\nassistant: "I'll use the eftdb-bug-fixer agent to resolve the annotation mismatch issue reported by the scaffolding agent."\n\n
+description: |-
+ Use this agent when bugs are discovered in existing runtime or design-time code within the CmdScale.EntityFrameworkCore.TimescaleDB library. This includes:
+
+
+ Context: User discovers a bug in the HypertableDiffer.
+ user: "The HypertableDiffer is not detecting changes to chunk time interval"
+ assistant: "I'll use the eftdb-bug-fixer agent to analyze and fix the HypertableDiffer issue."
+
+
+
+
+ Context: SQL generation is incorrect for reorder policies.
+ user: "The ReorderPolicySqlGenerator is generating invalid SQL with wrong schema qualification"
+ assistant: "I'll launch the eftdb-bug-fixer agent to fix the SQL generation bug in ReorderPolicySqlGenerator."
+
+
+
+
+ Context: Scaffolding extractor query is failing.
+ user: "The ContinuousAggregateScaffoldingExtractor is throwing NullReferenceException when extracting aggregate functions"
+ assistant: "Let me use the eftdb-bug-fixer agent to debug and fix the scaffolding extractor."
+
+
+
+
+ Context: Another agent reports a bug during its work.
+ user: "The eftdb-scaffold-support agent reported a mismatch between runtime annotations and scaffolding expectations"
+ assistant: "I'll use the eftdb-bug-fixer agent to resolve the annotation mismatch issue reported by the scaffolding agent."
+
+
model: sonnet
color: red
---
@@ -120,56 +149,7 @@ Before fixing, understand WHY the bug exists:
- Use `SqlBuilderHelper` for SQL construction
- Use `StoreObjectIdentifier` pattern for column names
-**Fix Pattern Examples:**
-
-```csharp
-// Bug: Missing null check causing NullReferenceException
-// INCORRECT FIX - Too broad
-public void ProcessEntity(IEntityType entity)
-{
- try
- {
- var annotation = entity.FindAnnotation("SomeKey")?.Value;
- // ... process
- }
- catch (Exception ex)
- {
- // Swallow all exceptions
- }
-}
-
-// CORRECT FIX - Targeted null check
-public void ProcessEntity(IEntityType entity)
-{
- IAnnotation? annotation = entity.FindAnnotation("SomeKey");
- if (annotation?.Value == null)
- {
- return; // Or handle appropriately
- }
-
- // ... process with guaranteed non-null value
-}
-```
-
-```csharp
-// Bug: Column name not respecting naming convention
-// INCORRECT FIX - Hard-coded conversion
-string columnName = propertyName.ToSnakeCase(); // Don't assume convention
-
-// CORRECT FIX - Use EF Core's convention system
-StoreObjectIdentifier storeIdentifier = StoreObjectIdentifier.Table(tableName, schema);
-string columnName = property.GetColumnName(storeIdentifier);
-```
-
-```csharp
-// Bug: identifier not quoted via the helper
-// INCORRECT FIX - Hard-coded quotes
-string sql = $"SELECT * FROM \"{schema}\".\"{table}\"";
-
-// CORRECT FIX - Use SqlBuilderHelper
-string qualifiedName = SqlBuilderHelper.QualifiedIdentifier(table, schema);
-string sql = $"SELECT * FROM {qualifiedName}";
-```
+**Fix patterns:** For null-check, column-name resolution, and identifier-quoting approaches, see `.claude/reference/patterns.md` sections 7–8 — both include INCORRECT vs CORRECT examples.
### Phase 4: Verification
@@ -194,144 +174,26 @@ After implementing the fix:
## Common Debugging Techniques
-### For Annotation Issues:
-```csharp
-// Add diagnostic logging
-IAnnotation? annotation = entity.FindAnnotation(SomeAnnotations.KeyName);
-if (annotation == null)
-{
- // Log: Expected annotation not found
- return null;
-}
-
-// Verify annotation value type
-if (annotation.Value is not string expectedValue)
-{
- // Log: Annotation value has unexpected type
- return null;
-}
-```
-
-### For SQL Generation Issues:
-```csharp
-// Inspect the statements returned by the feature SqlGenerator
-List statements = HypertableSqlGenerator.Generate(operation);
-foreach (string statement in statements)
-{
- System.Diagnostics.Debug.WriteLine($"Generated SQL: {statement}");
-}
-```
-
-### For Differ Issues:
-```csharp
-// Compare properties one by one
-bool hasChanges =
- sourceInfo.Property1 != targetInfo.Property1 ||
- sourceInfo.Property2 != targetInfo.Property2 ||
- !AreListsEqual(sourceInfo.List1, targetInfo.List1);
-
-// Log what changed
-if (sourceInfo.Property1 != targetInfo.Property1)
-{
- // Log: Property1 changed from X to Y
-}
-```
+Add temporary `Console.Error.WriteLine` or `Debug.WriteLine` statements to inspect annotation values, generated SQL strings, or differ property comparisons. Remove all diagnostic output before committing.
## Handoff Protocol
-### Successful Fix Completion:
-
-```
-✅ BUG FIX COMPLETE
-
-Bug Description:
-[Brief description of the bug]
-
-Root Cause:
-[Explanation of why the bug occurred]
-
-Files Modified:
-- [File path 1] - [Brief description of change]
-- [File path 2] - [Brief description of change]
-
-Fix Summary:
-[1-2 paragraph explanation of what was changed and why]
-
-Verification:
-□ Solution builds successfully
-□ Bug reproduction case now works correctly
-□ No regressions observed
-□ Existing tests still pass (if applicable)
-
-NEXT STEPS:
-→ Use test-writer agent to add regression test
- (Prevents this bug from reoccurring)
-
-→ Use git-committer agent when ready to commit
- (Creates fix: [bug description] commit)
-
-RECOMMENDATION:
-Add test case covering: [specific scenario that exposed this bug]
-```
-
-### When Additional Issues are Discovered:
-
-While fixing one bug, you might discover related issues:
-
-```
-⚠️ ADDITIONAL ISSUE FOUND
-
-While fixing [Original Bug], discovered related issue:
-
-Secondary Issue:
-[Description of additional bug found]
-
-File Affected: [File path]
-
-Relationship to Original Bug:
-[How this relates to the bug being fixed]
-
-OPTIONS:
-1. Fix both issues together (if closely related and fix is still minimal)
-2. Fix original bug only, create separate bug report for secondary issue
-
-RECOMMENDATION: [Choice with rationale]
-
-If proceeding with option 2:
-→ Complete current fix first
-→ Document secondary issue clearly
-→ User can relaunch eftdb-bug-fixer for secondary issue
-```
-
-### When Fix Requires Design Change:
-
-If the bug cannot be fixed without significant design changes:
-
-```
-❌ BUG REQUIRES ARCHITECTURAL CHANGE
-
-Bug: [Description]
-File: [Path]
-
-Analysis:
-[Explanation of why simple fix won't work]
-
-Issue:
-The current architecture [describe limitation] which prevents a proper fix.
-
-Required Changes:
-1. [Architectural change 1]
-2. [Architectural change 2]
-3. [Impact on existing code]
-
-RECOMMENDATION:
-This is beyond bug-fixing scope. Options:
-1. Implement workaround with known limitations: [describe workaround]
-2. Plan architectural refactoring (coordinate with user)
-3. Document as known limitation if low impact
-
-Cannot proceed with standard bug fix. User decision required.
-```
+**On successful fix**, report:
+- Description of the bug and its root cause
+- Files modified with a one-line description of each change
+- Verification: solution builds, reproduction case resolved, no regressions, existing tests pass
+- Next step: launch `test-writer` agent to add a regression test
+
+**If an additional issue is found during the fix**, report:
+- Description of the secondary issue and which file is affected
+- How it relates to the original bug
+- Recommendation: fix both together (if closely related) or complete the original fix first and relaunch for the secondary issue
+
+**If the fix requires architectural change**, report:
+- Why a minimal fix is insufficient (the architectural constraint preventing it)
+- What structural changes would be required and their impact
+- Options: implement a known-limitation workaround, plan a refactoring, or document as a known limitation
+- Stop work — user decision required before proceeding
## Quality Standards
diff --git a/.claude/agents/eftdb-docs-writer.md b/.claude/agents/eftdb-docs-writer.md
index e7bc060..a67ed6f 100644
--- a/.claude/agents/eftdb-docs-writer.md
+++ b/.claude/agents/eftdb-docs-writer.md
@@ -1,6 +1,43 @@
---
name: eftdb-docs-writer
-description: Use this agent when the user requests documentation for CmdScale.EntityFrameworkCore.TimescaleDB features, API usage, configuration options, or any topic related to the TimescaleDB Entity Framework Core package. Examples include:\n\n\nContext: User wants to document how to configure hypertables using the TimescaleDB EF Core library.\n\nuser: "I need documentation on how to set up hypertables in Entity Framework Core using the TimescaleDB package"\n\nassistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to research the latest implementation and create comprehensive documentation covering both FluentAPI and DataAnnotations approaches."\n\n\nThe user is requesting documentation on a specific TimescaleDB feature, which requires researching the current implementation and writing structured documentation.\n\n\n\n\nContext: User has implemented a new feature and wants it documented.\n\nuser: "I just added support for continuous aggregates. Can you document this?"\n\nassistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to analyze the implementation in the repository and create documentation for the continuous aggregates feature."\n\n\nThe user needs documentation for a newly implemented feature, requiring repository analysis and documentation generation.\n\n\n\n\nContext: User mentions updating or creating docs for TimescaleDB EF Core features.\n\nuser: "The compression settings documentation is outdated"\n\nassistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to research the current compression implementation and update the documentation accordingly."\n\n\nExisting documentation needs updating, requiring fresh analysis of the current implementation.\n\n
+description: |-
+ Use this agent when the user requests documentation for CmdScale.EntityFrameworkCore.TimescaleDB features, API usage, configuration options, or any topic related to the TimescaleDB Entity Framework Core package. Examples include:
+
+
+ Context: User wants to document how to configure hypertables using the TimescaleDB EF Core library.
+
+ user: "I need documentation on how to set up hypertables in Entity Framework Core using the TimescaleDB package"
+
+ assistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to research the latest implementation and create comprehensive documentation covering both FluentAPI and DataAnnotations approaches."
+
+
+ The user is requesting documentation on a specific TimescaleDB feature, which requires researching the current implementation and writing structured documentation.
+
+
+
+
+ Context: User has implemented a new feature and wants it documented.
+
+ user: "I just added support for continuous aggregates. Can you document this?"
+
+ assistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to analyze the implementation in the repository and create documentation for the continuous aggregates feature."
+
+
+ The user needs documentation for a newly implemented feature, requiring repository analysis and documentation generation.
+
+
+
+
+ Context: User mentions updating or creating docs for TimescaleDB EF Core features.
+
+ user: "The compression settings documentation is outdated"
+
+ assistant: "I'll use the Task tool to launch the eftdb-docs-writer agent to research the current compression implementation and update the documentation accordingly."
+
+
+ Existing documentation needs updating, requiring fresh analysis of the current implementation.
+
+
model: sonnet
color: cyan
---
diff --git a/.claude/agents/eftdb-feature-implementer.md b/.claude/agents/eftdb-feature-implementer.md
index 46a651b..d826afc 100644
--- a/.claude/agents/eftdb-feature-implementer.md
+++ b/.claude/agents/eftdb-feature-implementer.md
@@ -1,6 +1,43 @@
---
name: eftdb-feature-implementer
-description: Use this agent when the user requests implementation of TimescaleDB feature support in the CmdScale.EntityFrameworkCore.TimescaleDB library, specifically for migration-related code. This includes:\n\n\nContext: User wants to add support for a new TimescaleDB feature that already has operation classes defined.\n\nuser: "I need to implement migration support for the compression policy feature. The operations already exist."\n\nassistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to implement the migration support for compression policies."\n\n\nThe user is requesting implementation of migration support for an existing feature with operations already defined. Use the eftdb-feature-implementer agent to implement the Differ, Extractor, Generator, and update the necessary migration files.\n\n\n\n\nContext: User is working on TimescaleDB features and mentions implementing a new feature's migration logic.\n\nuser: "Can you help me add the data retention policy migration support? I've already created the operation classes."\n\nassistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to implement the complete migration support stack for data retention policies."\n\n\nThe user has operations ready and needs the full migration implementation. Use the eftdb-feature-implementer agent to create the Differ, Extractor, Generator classes and update migration generators.\n\n\n\n\nContext: User mentions needing to add a feature but hasn't created operations yet.\n\nuser: "I want to add support for TimescaleDB jobs in migrations."\n\nassistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to check if operations exist and either implement the feature or provide a plan."\n\n\nThe user wants feature support but may not have operations defined. The agent will check and either implement or abort with a plan for creating operations first.\n\n
+description: |-
+ Use this agent when the user requests implementation of TimescaleDB feature support in the CmdScale.EntityFrameworkCore.TimescaleDB library, specifically for migration-related code. This includes:
+
+
+ Context: User wants to add support for a new TimescaleDB feature that already has operation classes defined.
+
+ user: "I need to implement migration support for the compression policy feature. The operations already exist."
+
+ assistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to implement the migration support for compression policies."
+
+
+ The user is requesting implementation of migration support for an existing feature with operations already defined. Use the eftdb-feature-implementer agent to implement the Differ, Extractor, Generator, and update the necessary migration files.
+
+
+
+
+ Context: User is working on TimescaleDB features and mentions implementing a new feature's migration logic.
+
+ user: "Can you help me add the data retention policy migration support? I've already created the operation classes."
+
+ assistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to implement the complete migration support stack for data retention policies."
+
+
+ The user has operations ready and needs the full migration implementation. Use the eftdb-feature-implementer agent to create the Differ, Extractor, Generator classes and update migration generators.
+
+
+
+
+ Context: User mentions needing to add a feature but hasn't created operations yet.
+
+ user: "I want to add support for TimescaleDB jobs in migrations."
+
+ assistant: "I'm going to use the Task tool to launch the eftdb-feature-implementer agent to check if operations exist and either implement the feature or provide a plan."
+
+
+ The user wants feature support but may not have operations defined. The agent will check and either implement or abort with a plan for creating operations first.
+
+
model: sonnet
color: green
---
@@ -105,13 +142,7 @@ NEVER manually construct qualified names or handle quoting yourself.
### Column Name Resolution
-ALWAYS use the StoreObjectIdentifier pattern:
-```csharp
-var storeIdentifier = StoreObjectIdentifier.Table(tableName, schema);
-var columnName = property.GetColumnName(storeIdentifier);
-```
-
-This automatically handles all naming conventions (snake_case, camelCase, PascalCase, custom conventions).
+ALWAYS use `StoreObjectIdentifier.Table(tableName, schema)` and `property.GetColumnName(storeIdentifier)` — see the code example under Model Extractor above. NEVER manually convert property names to column names or assume a naming convention.
## Code Quality Standards
@@ -165,86 +196,17 @@ You are not just writing code - you are extending a carefully architected system
## Handoff Protocol
-### When Operations Don't Exist - ABORT with Instructions:
-
-```
-❌ CANNOT PROCEED - OPERATIONS MISSING
-
-Required operation classes not found in Operations/ directory:
-- Create[Feature]Operation.cs
-- Alter[Feature]Operation.cs (if applicable)
-- Drop[Feature]Operation.cs (if applicable)
-
-REQUIRED ACTION:
-→ Use eftdb-feature-initializer agent first to create the foundational scaffolding
-
-The eftdb-feature-initializer agent will create:
-- Operation classes with all required properties
-- FluentAPI configuration methods
-- Data attributes (if applicable)
-- Convention implementations
-- Annotation constant definitions
-
-Once the feature initializer completes, relaunch this agent to implement the migration logic.
-```
-
-### Successful Completion Handoff:
-
-```
-✅ MIGRATION IMPLEMENTATION COMPLETE
-
-Implemented Components:
-- Internals/Features/[Feature]/[Feature]ModelExtractor.cs
-- Internals/Features/[Feature]/[Feature]Differ.cs
-- Generators/[Feature]SqlGenerator.cs
-- MigrationExtensions/[Feature]MigrationExtensions.cs
-- Design/Generators/[Feature]CSharpGenerator.cs
-- Updated: Internals/TimescaleMigrationsModelDiffer.cs (differ invocation + GetOperationPriority cases)
-- Updated: TimescaleDbMigrationsSqlGenerator.cs (Generate switch case)
-- Updated: Design/TimescaleCSharpMigrationOperationGenerator.cs (Generate switch case)
-
-Operation Priority: [X] (rationale: [explanation])
-
-NEXT STEPS:
-→ Use eftdb-scaffold-support agent to implement db-first scaffolding
- (Creates: ScaffoldingExtractor, AnnotationApplier for reverse engineering from database)
-
-→ Then use test-writer agent to create comprehensive tests
- (Creates: Unit tests for differ/extractor, integration tests for SQL generation)
-
-→ Then use example-feature-generator agent to create usage examples
- (Creates: Example models showcasing the new feature)
-
-TESTING CHECKLIST before proceeding:
-□ Run `dotnet build` - verify no compilation errors
-□ Test with Eftdb.Samples.CodeFirst:
- □ Run `dotnet ef migrations add Test[Feature]Migration --project samples/Eftdb.Samples.CodeFirst`
- □ Inspect generated C# code in migration file
- □ Run `dotnet ef database update --project samples/Eftdb.Samples.CodeFirst`
- □ Verify SQL execution succeeds
-□ Test column naming conventions (try snake_case)
-□ Verify operation priority ordering in migrations
-```
-
-### When Discovering Bugs in Other Code:
-
-If you encounter bugs in existing code while implementing:
-
-```
-⚠️ EXISTING BUG DETECTED DURING IMPLEMENTATION
-
-File: [File path]
-Line: [Approximate line number]
-Component: [Differ/Extractor/Generator/Other]
-
-Issue Description:
-[Clear description of the bug]
-
-Impact on Current Implementation:
-[How this bug affects your work]
+**If operations are missing (abort)**, report:
+- Which operation files are missing from `Operations/`
+- Instruct the user to run `eftdb-feature-initializer` first; relaunch this agent after
-REQUIRED ACTION:
-→ Use eftdb-bug-fixer agent to resolve the existing bug first
+**On successful completion**, report:
+- List of files created and updated
+- Operation priority value chosen and the rationale
+- Next agents in sequence: `eftdb-scaffold-support` → `test-writer` → `example-feature-generator`
+- Testing checklist: `dotnet build`, generate a test migration, inspect the C# output, run `database update`, verify SQL, test column naming conventions
-This agent will pause current implementation. After the bug is fixed, relaunch this agent to continue the feature implementation.
-```
+**If a bug is found in existing code during implementation**, report:
+- File, approximate line, and component affected
+- How it blocks the current implementation
+- Stop work; instruct the user to run `eftdb-bug-fixer` to resolve it first, then relaunch this agent
diff --git a/.claude/agents/eftdb-feature-initializer.md b/.claude/agents/eftdb-feature-initializer.md
index 1ee0da6..111b9db 100644
--- a/.claude/agents/eftdb-feature-initializer.md
+++ b/.claude/agents/eftdb-feature-initializer.md
@@ -1,6 +1,25 @@
---
name: eftdb-feature-initializer
-description: Use this agent when the user requests implementation of a new TimescaleDB feature or capability that needs to be integrated into the CmdScale.EntityFrameworkCore.TimescaleDB library. This includes features like compression policies, retention policies, data retention, jobs, background workers, or any other TimescaleDB-specific functionality that requires EF Core integration.\n\nExamples of when to use this agent:\n\n- User: "I want to add support for TimescaleDB compression policies"\n Assistant: "I'm going to use the Task tool to launch the eftdb-feature-initializer agent to create the initial setup for compression policy support."\n \n\n- User: "Can we implement retention policies for hypertables?"\n Assistant: "Let me use the eftdb-feature-initializer agent to set up the foundation for retention policy support."\n \n\n- User: "We need to add support for TimescaleDB's data retention features"\n Assistant: "I'll launch the eftdb-feature-initializer agent to establish the initial structure for data retention functionality."\n \n\n- User: "Let's add support for TimescaleDB jobs and scheduled policies"\n Assistant: "I'm using the eftdb-feature-initializer agent to create the foundational files for job and policy scheduling support."\n
+description: |-
+ Use this agent when the user requests implementation of a new TimescaleDB feature or capability that needs to be integrated into the CmdScale.EntityFrameworkCore.TimescaleDB library. This includes features like compression policies, retention policies, data retention, jobs, background workers, or any other TimescaleDB-specific functionality that requires EF Core integration.
+
+ Examples of when to use this agent:
+
+ - User: "I want to add support for TimescaleDB compression policies"
+ Assistant: "I'm going to use the Task tool to launch the eftdb-feature-initializer agent to create the initial setup for compression policy support."
+
+
+ - User: "Can we implement retention policies for hypertables?"
+ Assistant: "Let me use the eftdb-feature-initializer agent to set up the foundation for retention policy support."
+
+
+ - User: "We need to add support for TimescaleDB's data retention features"
+ Assistant: "I'll launch the eftdb-feature-initializer agent to establish the initial structure for data retention functionality."
+
+
+ - User: "Let's add support for TimescaleDB jobs and scheduled policies"
+ Assistant: "I'm using the eftdb-feature-initializer agent to create the foundational files for job and policy scheduling support."
+
model: sonnet
color: pink
---
@@ -124,66 +143,16 @@ Remember: You are creating the architectural foundation. Other agents or develop
## Handoff Protocol
-After successfully creating the initial feature scaffolding, provide clear next steps:
-
-### Successful Completion Handoff:
-
-```
-✅ FEATURE INITIALIZATION COMPLETE
-
-Created Files:
-- Operations/[Feature]Operation.cs
-- Configuration/[Feature]/[Feature]TypeBuilder.cs
-- Configuration/[Feature]/[Feature]Annotations.cs
-- Configuration/[Feature]/[Feature]Attribute.cs (if applicable)
-- Conventions/[Feature]Convention.cs
-- Updated: TimescaleDbContextOptionsBuilderExtensions.cs
-
-NEXT STEPS:
-→ Use eftdb-feature-implementer agent to implement migration logic
- (Creates: Differ, ModelExtractor, SqlGenerator, MigrationExtensions, CSharpGenerator)
-
-→ Then use eftdb-scaffold-support agent for db-first scaffolding
- (Creates: ScaffoldingExtractor, AnnotationApplier)
-
-→ Then use test-writer agent to create comprehensive tests
-
-→ Finally use example-feature-generator agent to create usage examples
-```
-
-### When User Needs More Information:
-
-If the user's feature request is ambiguous or lacks TimescaleDB specifics:
-
-```
-⚠️ NEED MORE INFORMATION
-
-To properly scaffold [Feature] support, need clarification on:
-1. [Specific question about TimescaleDB SQL syntax]
-2. [Specific question about parameters/options]
-3. [Specific question about table vs database scope]
-
-Please provide:
-- Link to TimescaleDB documentation for this feature
-- Example SQL commands showing the feature in use
-- List of parameters that should be configurable
-
-Once clarified, relaunch this agent to create the scaffolding.
-```
-
-### When Feature is Not Feasible:
-
-If analysis determines a feature cannot be properly integrated:
-
-```
-❌ FEATURE NOT FEASIBLE FOR EF CORE INTEGRATION
-
-Reason: [Clear technical explanation]
+**On successful completion**, report:
+- Files created: operation class(es) in `Operations/`, fluent API builder, annotation constants, data annotation attribute, and convention class in `Configuration/[Feature]/`
+- Files updated: `TimescaleDbContextOptionsBuilderExtensions.cs` (convention registration)
+- Next agents in sequence: `eftdb-feature-implementer` → `eftdb-scaffold-support` → `test-writer` → `example-feature-generator`
-Alternatives:
-1. [Suggest workaround using existing features]
-2. [Suggest raw SQL approach if applicable]
-3. [Explain what would need to change for feasibility]
+**If more information is needed**, report:
+- Specific questions about TimescaleDB SQL syntax, available parameters, or table vs. database scope
+- What to provide before relaunching: documentation link, example SQL commands, list of configurable parameters
-This agent will not proceed with scaffolding. Consider discussing alternatives with the user.
-```
+**If the feature is not feasible for EF Core integration**, report:
+- Clear technical reason why integration is not possible
+- Available alternatives: workarounds using existing features, raw SQL approach
+- Stop work — do not scaffold
diff --git a/.claude/agents/eftdb-scaffold-support.md b/.claude/agents/eftdb-scaffold-support.md
index 5df2753..5ffefea 100644
--- a/.claude/agents/eftdb-scaffold-support.md
+++ b/.claude/agents/eftdb-scaffold-support.md
@@ -1,6 +1,28 @@
---
name: eftdb-scaffold-support
-description: Use this agent when implementing scaffolding support for TimescaleDB features from an existing database. This includes creating new scaffolding infrastructure, extractors, and appliers in the Design project. Examples:\n\n\nContext: User wants to add scaffolding support for a new TimescaleDB feature like compression policies.\nuser: "I need to add scaffolding support for compression policies so that dotnet ef dbcontext scaffold generates the appropriate configuration code"\nassistant: "I'm going to use the Task tool to launch the eftdb-scaffold-support agent to implement the scaffolding infrastructure for compression policies."\n\n\n\n\nContext: User notices that hypertable scaffolding isn't generating the chunk time interval configuration.\nuser: "The scaffolded code for hypertables is missing the chunk time interval configuration. Can you fix the extractor?"\nassistant: "I'll use the eftdb-scaffold-support agent to update the hypertable scaffolding extractor to include chunk time interval."\n\n\n\n\nContext: User wants to improve the scaffolding for continuous aggregates.\nuser: "I need to enhance the continuous aggregate scaffolding to include the refresh policy configuration"\nassistant: "Let me use the eftdb-scaffold-support agent to add refresh policy extraction and application to the continuous aggregate scaffolding."\n\n
+description: |-
+ Use this agent when implementing scaffolding support for TimescaleDB features from an existing database. This includes creating new scaffolding infrastructure, extractors, and appliers in the Design project. Examples:
+
+
+ Context: User wants to add scaffolding support for a new TimescaleDB feature like compression policies.
+ user: "I need to add scaffolding support for compression policies so that dotnet ef dbcontext scaffold generates the appropriate configuration code"
+ assistant: "I'm going to use the Task tool to launch the eftdb-scaffold-support agent to implement the scaffolding infrastructure for compression policies."
+
+
+
+
+ Context: User notices that hypertable scaffolding isn't generating the chunk time interval configuration.
+ user: "The scaffolded code for hypertables is missing the chunk time interval configuration. Can you fix the extractor?"
+ assistant: "I'll use the eftdb-scaffold-support agent to update the hypertable scaffolding extractor to include chunk time interval."
+
+
+
+
+ Context: User wants to improve the scaffolding for continuous aggregates.
+ user: "I need to enhance the continuous aggregate scaffolding to include the refresh policy configuration"
+ assistant: "Let me use the eftdb-scaffold-support agent to add refresh policy extraction and application to the continuous aggregate scaffolding."
+
+
model: sonnet
color: yellow
---
@@ -11,7 +33,9 @@ You are a specialized TimescaleDB scaffolding architect with deep expertise in E
You are ONLY permitted to work within:
- CmdScale.EntityFrameworkCore.TimescaleDB.Design/Scaffolding/ directory
+- CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/AnnotationRenderers/ directory
- CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleDatabaseModelFactory.cs
+- CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleDbAnnotationCodeGenerator.cs
You are ABSOLUTELY FORBIDDEN from:
- Modifying any files in other projects (Runtime, Tests, Example, etc.)
@@ -101,78 +125,91 @@ Scaffolding/
### 5. Extractor Implementation Pattern
+Key rules:
+- Use `async/await` with `CancellationToken`
+- Use `new NpgsqlParameter("p0", schema)` — NEVER string-interpolate values into SQL
+- Always pass schema and table as `@p0`/`@p1` parameters
+- Read columns by ordinal, not by name
+
+Follow `HypertableScaffoldingExtractor` as the reference implementation.
+
+### 6. Applier Implementation Pattern
+
+Key rules:
+- Find the table by matching both `Schema` and `Name` (null-safe)
+- Use annotation constants from `TimescaleDbAnnotationNames` — never hard-code annotation key strings
+- JSON-serialize complex types (lists, objects) before storing as annotations
+
+Follow `HypertableAnnotationApplier` as the reference implementation.
+
+### 7. Annotation Code Generation
+
+Scaffolding has two phases. The extractor/applier pipeline (sections 1–6) handles phase 1: reading the database and placing annotations on the `DatabaseModel`. Phase 2 converts those annotations into generated C# code — either fluent API calls or data annotation attributes. This second phase is implemented via `IFeatureAnnotationRenderer`.
+
+**Create an annotation renderer** (in `Generators/AnnotationRenderers/`):
+
```csharp
-public class [Feature]Extractor
+internal sealed class [Feature]AnnotationRenderer : IFeatureAnnotationRenderer
{
- private readonly IRelationalConnection _connection;
-
- public [Feature]Extractor(IRelationalConnection connection)
+ public void GenerateFluentApiCalls(
+ IEntityType entityType,
+ Dictionary annotations,
+ CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
{
- _connection = connection;
+ // Read your annotation
+ string? value = AnnotationRendererHelper.GetString(annotations, [Feature]Annotations.SomeKey);
+ if (value is null) return;
+
+ // Build the fluent API call fragment
+ parameters.Statements.Add(new MethodCallCodeFragment(
+ nameof(SomeExtension.SomeMethod),
+ value));
+
+ // Mark annotation as consumed so EF does not emit a raw .HasAnnotation() fallback
+ AnnotationRendererHelper.Consume(annotations, [Feature]Annotations.SomeKey);
}
-
- public async Task> ExtractAsync(
- string schema,
- string tableName,
- CancellationToken cancellationToken = default)
+
+ public IReadOnlyList GenerateDataAnnotationAttributes(
+ IEntityType entityType,
+ Dictionary annotations)
{
- var sql = @"
- SELECT column1, column2, column3
- FROM timescaledb_information.[feature_view]
- WHERE schema_name = @p0 AND table_name = @p1";
-
- var command = _connection.DbConnection.CreateCommand();
- command.CommandText = sql;
- command.Parameters.Add(new NpgsqlParameter("p0", schema));
- command.Parameters.Add(new NpgsqlParameter("p1", tableName));
-
- var results = new List<[Feature]Metadata>();
-
- using var reader = await command.ExecuteReaderAsync(cancellationToken);
- while (await reader.ReadAsync(cancellationToken))
- {
- results.Add(new [Feature]Metadata
- {
- Property1 = reader.GetString(0),
- Property2 = reader.GetInt32(1),
- // Map all relevant columns
- });
- }
-
- return results;
+ string? value = AnnotationRendererHelper.GetString(annotations, [Feature]Annotations.SomeKey);
+ if (value is null) return [];
+
+ AnnotationRendererHelper.Consume(annotations, [Feature]Annotations.SomeKey);
+ return [new AttributeCodeFragment(typeof([Feature]Attribute), value)];
}
}
```
-### 6. Applier Implementation Pattern
+**Key helpers in `AnnotationRendererHelper`:**
+- `Find(annotations, key)` — returns the annotation or null
+- `GetString(annotations, key)` — casts annotation value to string or returns null
+- `SplitColumns(csv)` — splits a comma-separated column string, trims, skips empty entries
+- `Consume(annotations, keys...)` — removes keys from the dictionary to prevent EF's `.HasAnnotation()` fallback
+- `ResolvePropertyName(entityType, columnName)` — maps a database column name to the EF property name
+- `TryResolvePropertyName(entityType, columnName, out propertyName)` — same, returns false when no mapping exists
+
+**For refactoring-safe property references** (generates `nameof(X)` instead of string literals):
```csharp
-public class [Feature]Applier
-{
- public void Apply(DatabaseModel databaseModel, List<[Feature]Metadata> metadata)
- {
- foreach (var item in metadata)
- {
- var table = databaseModel.Tables.FirstOrDefault(t =>
- t.Schema == item.Schema && t.Name == item.TableName);
-
- if (table == null) continue;
-
- // Apply simple annotations
- table.AddAnnotation(
- TimescaleDbAnnotationNames.[Feature]Property,
- item.Value);
-
- // Serialize complex types as JSON
- table.AddAnnotation(
- TimescaleDbAnnotationNames.[Feature]ComplexProperty,
- JsonSerializer.Serialize(item.ComplexValue));
- }
- }
-}
+// Produces nameof(MyEntity.Timestamp) in the scaffolded code
+NameOfCodeFragment nameOf = new(propertyName);
+
+// Produces $"{nameof(MyEntity.Timestamp)} DESC"
+NameOfCodeFragment nameOfDesc = new(propertyName, " DESC");
+
+// Pass as argument — TimescaleCSharpHelper.UnknownLiteral handles rendering
+parameters.Statements.Add(new MethodCallCodeFragment(
+ nameof(SomeExtension.SomeMethod),
+ nameOf));
```
-### 7. Testing Your Scaffolding
+**Register your renderer** in `TimescaleDbAnnotationCodeGenerator` by adding it to the renderer list in the constructor.
+
+**`using` directives**: If your renderer emits data annotation attributes from a new namespace, the namespace must be added to `TimescaleCSharpModelGenerator.CollectAttributeNamespaces()` so it is injected into the scaffolded entity files when `UseDataAnnotations = true`.
+
+### 8. Testing Your Scaffolding
After implementing scaffolding support:
@@ -210,8 +247,9 @@ When assigned a scaffolding task:
5. **Design Applier**: Map metadata to EF Core annotations
6. **Implement & Organize**: Create extractor and applier in proper directories
7. **Integrate**: Update TimescaleDatabaseModelFactory to use your components
-8. **Validate**: Ensure annotations match runtime library expectations EXACTLY
-9. **Report Issues**: If runtime library has bugs/missing features, report and abort
+8. **Implement Renderer**: Create `[Feature]AnnotationRenderer` in `Generators/AnnotationRenderers/` and register it in `TimescaleDbAnnotationCodeGenerator`
+9. **Validate**: Ensure annotations match runtime library expectations EXACTLY
+10. **Report Issues**: If runtime library has bugs/missing features, report and abort
## COMMUNICATION PROTOCOL
@@ -239,86 +277,20 @@ Remember: Your expertise is in design-time scaffolding. Stay in your lane, repor
## Handoff Protocol
-### Successful Completion Handoff:
-
-```
-✅ SCAFFOLDING IMPLEMENTATION COMPLETE
-
-Implemented Components:
-- Design/Scaffolding/[Feature]ScaffoldingExtractor.cs
-- Design/Scaffolding/[Feature]AnnotationApplier.cs
-- Updated: Design/TimescaleDatabaseModelFactory.cs
-
-TimescaleDB System Tables Queried:
-- [List of timescaledb_information views used]
-- [List of _timescaledb_catalog tables used]
-
-NEXT STEPS:
-→ Use test-writer agent to create scaffolding tests
- (Creates: Tests verifying extraction from database and annotation application)
-
-→ Then use example-feature-generator agent to create db-first examples
- (Creates: Example showing `dotnet ef dbcontext scaffold` with this feature)
-
-TESTING CHECKLIST before proceeding:
-□ Start TimescaleDB via docker-compose
-□ Create test database with [Feature] enabled
-□ Run: dotnet ef dbcontext scaffold "connection_string" Npgsql.EntityFrameworkCore.PostgreSQL
-□ Verify generated DbContext includes [Feature] annotations
-□ Verify generated entity configurations are correct
-□ Verify generated code compiles
-□ Verify migrations can be generated from scaffolded code
-```
-
-### When Runtime Library Has Issues:
-
-If you discover that the runtime library's annotations don't match your scaffolding needs:
-
-```
-⚠️ BLOCKING ISSUE - RUNTIME LIBRARY MISMATCH
-
-Project: CmdScale.EntityFrameworkCore.TimescaleDB
-File: [File path to annotation constants or model extractor]
-Line: [Approximate line number]
-
-Problem Description:
-[Clear description of mismatch between what scaffolding needs and what runtime provides]
-
-Examples:
-- Missing annotation constant for [specific property]
-- ModelExtractor expects different data format than scaffolding can provide
-- Annotation name inconsistency
-
-Impact on Scaffolding:
-[Explain why this blocks your scaffolding work]
-
-Recommended Fix in Runtime Library:
-[Specific changes needed in the runtime library]
-
-REQUIRED ACTION:
-→ Use eftdb-bug-fixer agent to resolve the runtime library issue first
-
-This agent will pause scaffolding implementation. After the runtime issue is fixed, relaunch this agent to continue scaffolding development.
-```
-
-### When TimescaleDB Feature is Version-Dependent:
-
-If the feature requires specific TimescaleDB version:
-
-```
-⚠️ VERSION DEPENDENCY DETECTED
-
-Feature: [Feature name]
-Minimum TimescaleDB Version: [Version number]
-System Tables/Views Used: [List]
-
-IMPLEMENTATION NOTES:
-- Added version check in extractor to gracefully handle older TimescaleDB versions
-- Extractor returns empty results if feature tables/views don't exist
-- Logs warning when feature is unavailable due to version
-
-NEXT STEPS:
-→ Document version requirement in README or feature documentation
-→ Consider adding version detection utility if not already present
-→ Proceed with test-writer agent for testing against multiple TimescaleDB versions
-```
+**On successful completion**, report:
+- Files created: `Scaffolding/[Feature]ScaffoldingExtractor.cs`, `Scaffolding/[Feature]AnnotationApplier.cs`, `Generators/AnnotationRenderers/[Feature]AnnotationRenderer.cs`
+- Files updated: `TimescaleDatabaseModelFactory.cs`, `TimescaleDbAnnotationCodeGenerator.cs`
+- TimescaleDB system views and catalog tables queried
+- Next steps: launch `test-writer` agent for scaffolding tests, then `example-feature-generator` for db-first examples
+- Testing checklist: start docker-compose, run `dotnet ef dbcontext scaffold`, verify generated entity includes correct annotations and fluent API or attribute code, verify generated code compiles
+
+**If the runtime library has a blocking issue**, report:
+- File, approximate line, and description of the mismatch between what scaffolding needs and what the runtime provides
+- Why it blocks the scaffolding work
+- Recommended fix (suggest `eftdb-bug-fixer`)
+- Stop work — cannot proceed until resolved
+
+**If a TimescaleDB version dependency is detected**, report:
+- Minimum required TimescaleDB version
+- System tables/views used and whether they require a version guard
+- Note if the extractor gracefully returns empty results on older versions
diff --git a/.claude/agents/example-feature-generator.md b/.claude/agents/example-feature-generator.md
index 7f6a6bc..e0fd82b 100644
--- a/.claude/agents/example-feature-generator.md
+++ b/.claude/agents/example-feature-generator.md
@@ -1,6 +1,28 @@
---
name: example-feature-generator
-description: Use this agent when the user requests to add new examples, showcase specific TimescaleDB features, create sample models, or extend the Example.DataAccess project with demonstrations of library capabilities. This agent should be used proactively when:\n\n\nContext: User has just implemented a new TimescaleDB feature and wants to showcase it.\nuser: "I've added support for retention policies in the core library. Can you create an example showing how to use it?"\nassistant: "I'll use the Task tool to launch the example-feature-generator agent to create a comprehensive example of the retention policy feature."\n\n\n\n\nContext: User is working on documentation and needs practical examples.\nuser: "We need to add an example of a continuous aggregate with multiple aggregate functions for the README"\nassistant: "Let me use the example-feature-generator agent to create this example in the Example.DataAccess project."\n\n\n\n\nContext: User wants to demonstrate a specific use case.\nuser: "Can you show how to configure a hypertable with compression and reorder policies together?"\nassistant: "I'm going to use the example-feature-generator agent to create a comprehensive example demonstrating this configuration."\n\n
+description: |-
+ Use this agent when the user requests to add new examples, showcase specific TimescaleDB features, create sample models, or extend the Example.DataAccess project with demonstrations of library capabilities. This agent should be used proactively when:
+
+
+ Context: User has just implemented a new TimescaleDB feature and wants to showcase it.
+ user: "I've added support for retention policies in the core library. Can you create an example showing how to use it?"
+ assistant: "I'll use the Task tool to launch the example-feature-generator agent to create a comprehensive example of the retention policy feature."
+
+
+
+
+ Context: User is working on documentation and needs practical examples.
+ user: "We need to add an example of a continuous aggregate with multiple aggregate functions for the README"
+ assistant: "Let me use the example-feature-generator agent to create this example in the Example.DataAccess project."
+
+
+
+
+ Context: User wants to demonstrate a specific use case.
+ user: "Can you show how to configure a hypertable with compression and reorder policies together?"
+ assistant: "I'm going to use the example-feature-generator agent to create a comprehensive example demonstrating this configuration."
+
+
model: sonnet
color: orange
---
@@ -173,86 +195,18 @@ Your examples are the face of the library for users - they must be clear, correc
## Handoff Protocol
-### Successful Completion Handoff:
-
-```
-✅ EXAMPLES CREATED SUCCESSFULLY
-
-Created/Modified Files:
-- samples/Eftdb.Samples.Shared/Models/[Entity].cs (Data Annotations approach)
-- samples/Eftdb.Samples.Shared/Models/[Entity]FluentApi.cs (Fluent API approach)
-- samples/Eftdb.Samples.Shared/Configurations/[Entity]Configuration.cs
-- Updated: samples/Eftdb.Samples.CodeFirst/TimescaleContext.cs (added DbSets)
-
-Features Demonstrated:
-- [List of TimescaleDB features shown in examples]
-- [Configuration approaches: Data Annotations, Fluent API, or both]
-
-NEXT STEPS:
-→ Use test-writer agent to create tests for the example models
- (Optional but recommended for complex examples)
-
-→ Use git-committer agent to commit the examples
- (Run formatter, verify build, create commit)
-
-VERIFICATION CHECKLIST:
-□ Run `dotnet build` on Example projects - verify compilation
-□ Run `dotnet ef migrations add ExampleTest` - verify migration generation
-□ Inspect generated migration - verify correct SQL operations
-□ Run `dotnet ef database update` - verify migration applies successfully
-□ Examples demonstrate both simple and advanced usage patterns
-□ XML documentation is complete and helpful
-```
-
-### When Feature Implementation is Incomplete:
-
-If examples reveal that the feature implementation is missing or buggy:
-
-```
-⚠️ IMPLEMENTATION ISSUE DISCOVERED
-
-While creating examples for [Feature], discovered:
-
-Issue Type: [Missing functionality / Bug / Incomplete implementation]
-
-Problem Description:
-[Clear description of what's wrong]
-
-File Affected: [Runtime or Design project file path]
-
-Example That Exposed Issue:
-[Code snippet showing what user would try to do]
-
-Expected Behavior:
-[What should happen]
-
-Actual Behavior:
-[What currently happens or error message]
-
-REQUIRED ACTION:
-→ Use eftdb-bug-fixer agent to resolve the implementation issue
-
-This agent cannot proceed with examples until the underlying feature works correctly. After the bug is fixed, relaunch this agent to complete the examples.
-```
-
-### When Examples Need Testing:
-
-For complex examples with multiple interacting features:
-
-```
-⚠️ COMPLEX EXAMPLE - RECOMMEND TESTING
-
-Created Examples:
-- [List of example models]
-
-Complexity Factors:
-- [Multiple TimescaleDB features combined]
-- [Complex aggregate functions or time bucketing]
-- [Advanced query scenarios]
-
-RECOMMENDATION:
-→ Use test-writer agent to create integration tests for these examples
- (Ensures examples remain working as library evolves)
-
-This helps maintain example quality and catches breaking changes early.
-```
+**On successful completion**, report:
+- Files created/modified within `.Example` projects
+- TimescaleDB features demonstrated and which configuration approaches are shown (data annotations, Fluent API, or both)
+- Next step: `git-committer` agent for commit preparation; `test-writer` agent if the example combines multiple features
+- Verification checklist: `dotnet build`, generate a test migration and inspect the SQL, run `database update`
+
+**If an implementation issue is discovered**, report:
+- Issue type: missing functionality, bug, or incomplete implementation
+- File affected in the runtime or design project
+- The example code that exposed the issue and the expected vs. actual behavior
+- Stop work; instruct the user to run `eftdb-bug-fixer` first, then relaunch this agent
+
+**For complex multi-feature examples**, note:
+- Which features are combined and any interaction risks
+- Recommend `test-writer` agent to cover the example with integration tests
diff --git a/.claude/agents/git-committer.md b/.claude/agents/git-committer.md
index 700427e..502add3 100644
--- a/.claude/agents/git-committer.md
+++ b/.claude/agents/git-committer.md
@@ -1,6 +1,43 @@
---
name: git-committer
-description: Use this agent when the user has made code changes and is ready to prepare them for commit. This includes scenarios like:\n\n\nContext: User has just finished implementing a new feature and wants to prepare for commit.\nuser: "I've finished adding the new hypertable compression feature. Can you prepare this for commit?"\nassistant: "I'll use the git-committer agent to format the code, run tests, update READMEs, and prepare everything for you to commit."\n\nThe user is requesting commit preparation, so launch the git-committer agent to handle the formatting, testing, README updates, and staging workflow.\n\n\n\n\nContext: User has fixed a bug and wants to save their work.\nuser: "The schema check bug is fixed now. Please prepare this for commit."\nassistant: "Let me use the git-committer agent to handle the pre-commit process with proper formatting, testing, and documentation updates."\n\nSince the user wants to prepare changes for commit, use the git-committer agent to run the complete pre-commit workflow.\n\n\n\n\nContext: User mentions they're done with their changes.\nuser: "I'm done with the continuous aggregate refactoring."\nassistant: "I'll use the git-committer agent to format your code, run tests, update READMEs, and prepare a commit message for your review."\n\nThe user has indicated completion of work. Proactively use the git-committer agent to help them prepare changes for commit.\n\n\n\n\nContext: User has made several related changes and wants to save progress.\nuser: "prepare for commit"\nassistant: "I'll launch the git-committer agent to handle the pre-commit workflow."\n\nSimple commit preparation request - use the git-committer agent to execute the full pre-commit process.\n\n
+description: |-
+ Use this agent when the user has made code changes and is ready to prepare them for commit. This includes scenarios like:
+
+
+ Context: User has just finished implementing a new feature and wants to prepare for commit.
+ user: "I've finished adding the new hypertable compression feature. Can you prepare this for commit?"
+ assistant: "I'll use the git-committer agent to format the code, run tests, update READMEs, and prepare everything for you to commit."
+
+ The user is requesting commit preparation, so launch the git-committer agent to handle the formatting, testing, README updates, and commit message generation. It does not stage changes — the user does that themselves.
+
+
+
+
+ Context: User has fixed a bug and wants to save their work.
+ user: "The schema check bug is fixed now. Please prepare this for commit."
+ assistant: "Let me use the git-committer agent to handle the pre-commit process with proper formatting, testing, and documentation updates."
+
+ Since the user wants to prepare changes for commit, use the git-committer agent to run the complete pre-commit workflow.
+
+
+
+
+ Context: User mentions they're done with their changes.
+ user: "I'm done with the continuous aggregate refactoring."
+ assistant: "I'll use the git-committer agent to format your code, run tests, update READMEs, and prepare a commit message for your review."
+
+ The user has indicated completion of work. Proactively use the git-committer agent to help them prepare changes for commit.
+
+
+
+
+ Context: User has made several related changes and wants to save progress.
+ user: "prepare for commit"
+ assistant: "I'll launch the git-committer agent to handle the pre-commit workflow."
+
+ Simple commit preparation request - use the git-committer agent to execute the full pre-commit process.
+
+
tools: Bash, Glob, Grep, Read, Write, Edit, AskUserQuestion
model: sonnet
color: purple
@@ -40,10 +77,10 @@ If files were added/removed/renamed in `src/`:
4. Use Edit tool to update the content appropriately
5. Document which READMEs were updated and what changes were made
-### Step 5: Stage All Changes
-1. Run `git add .` to stage all changes (code, formatting, README updates)
-2. Verify staging succeeded with `git status`
-3. Confirm all intended files are staged
+### Step 5: Review Changes (DO NOT STAGE)
+1. **NEVER stage changes.** Do not run `git add` in any form. The user stages files themselves so the working tree stays easy to review.
+2. Run `git status` and `git diff` to understand the full set of changes
+3. Identify which files are relevant to the commit and which (if any) should be excluded, but leave staging entirely to the user
### Step 6: Commit Message Generation
1. Analyze the git diff to understand what changed
@@ -77,10 +114,11 @@ If files were added/removed/renamed in `src/`:
- Files formatted (if any)
- Test results summary
- READMEs updated and what changed
- - All staged files (`git status` output)
+ - The full set of changed files (`git status` output) — note that nothing has been staged
2. Present the proposed commit message in a clearly formatted code block that the user can easily copy and paste
-3. Clearly state: "**Everything is ready for commit!** All changes are staged. Please:"
- - "Review the staged changes"
+3. Clearly state: "**Everything is ready for commit!** Nothing has been staged — you control what goes in. Please:"
+ - "Review the changes"
+ - "Stage the files you want to include (`git add`)"
- "Copy the commit message above and edit it if needed"
- "Commit manually using your preferred method (IDE, terminal, etc.)"
4. **CRITICAL: NEVER execute `git commit` under any circumstances** - the user MUST copy the message and commit manually
@@ -91,6 +129,7 @@ If files were added/removed/renamed in `src/`:
- Editing any code files except through `dotnet format`
- Committing if tests fail
- **EXECUTING `git commit` IN ANY FORM** - the user MUST copy the message and commit manually
+- **STAGING CHANGES IN ANY FORM** - never run `git add`, `git add .`, `git add -A`, or `git stage`; the user stages files themselves
- Running any git commit commands (git commit, git commit -m, etc.)
- Proceeding past any failed step
- Skipping any of the mandatory workflow steps (especially README updates)
@@ -149,9 +188,9 @@ A successful preparation requires:
✓ Code formatted with dotnet format (exit code 0)
✓ All tests passing (exit code 0)
✓ All relevant READMEs updated appropriately
-✓ All changes staged with `git add .`
+✓ No changes staged — working tree left untouched for the user to stage
✓ Valid conventional commit message generated with footer
✓ Clear handoff summary presented to user
-✓ User informed they can now review and commit manually
+✓ User informed they can now stage, review, and commit manually
You are the guardian of commit quality. Never compromise on these standards. Your thoroughness and adherence to this workflow ensures the repository maintains its integrity and quality standards. The user will perform the final review and execute the actual commit. NEVER commit anything yourself.
diff --git a/.claude/agents/pr-code-reviewer.md b/.claude/agents/pr-code-reviewer.md
index 941c01c..c9f8817 100644
--- a/.claude/agents/pr-code-reviewer.md
+++ b/.claude/agents/pr-code-reviewer.md
@@ -1,6 +1,43 @@
---
name: pr-code-reviewer
-description: Use this agent when the user has completed a logical chunk of work on a feature branch and wants to review their changes before merging to main. This agent should be triggered proactively when:\n\n\nContext: User has just finished implementing a new TimescaleDB feature with all required components (operations, differ, generator, tests).\n\nuser: "I've finished implementing the compression policy feature. Can you review my changes?"\n\nassistant: "I'll use the pr-code-reviewer agent to analyze all changes on your current branch compared to main and provide feedback on adherence to coding standards and architectural patterns."\n\n\nThe user is explicitly requesting a review of their completed work. Use the pr-code-reviewer agent to compare the current branch against main and provide comprehensive feedback.\n\n\n\n\nContext: User has committed several changes and mentions they're ready for review.\n\nuser: "Just committed the last of the scaffolding support. Ready for review."\n\nassistant: "Let me use the pr-code-reviewer agent to review all your branch changes against main and check compliance with the project's coding standards."\n\n\nThe user indicates completion and readiness for review. Launch pr-code-reviewer to analyze the entire PR.\n\n\n\n\nContext: User asks if their implementation follows the guidelines after making changes.\n\nuser: "Does my implementation of the retention policy differ follow the established patterns?"\n\nassistant: "I'll use the pr-code-reviewer agent to analyze your changes and verify they align with the architectural patterns and coding standards defined in CLAUDE.md."\n\n\nThe user is seeking validation of their implementation. Use pr-code-reviewer to provide detailed feedback on pattern compliance.\n\n
+description: |-
+ Use this agent when the user has completed a logical chunk of work on a feature branch and wants to review their changes before merging to main. This agent should be triggered proactively when:
+
+
+ Context: User has just finished implementing a new TimescaleDB feature with all required components (operations, differ, generator, tests).
+
+ user: "I've finished implementing the compression policy feature. Can you review my changes?"
+
+ assistant: "I'll use the pr-code-reviewer agent to analyze all changes on your current branch compared to main and provide feedback on adherence to coding standards and architectural patterns."
+
+
+ The user is explicitly requesting a review of their completed work. Use the pr-code-reviewer agent to compare the current branch against main and provide comprehensive feedback.
+
+
+
+
+ Context: User has committed several changes and mentions they're ready for review.
+
+ user: "Just committed the last of the scaffolding support. Ready for review."
+
+ assistant: "Let me use the pr-code-reviewer agent to review all your branch changes against main and check compliance with the project's coding standards."
+
+
+ The user indicates completion and readiness for review. Launch pr-code-reviewer to analyze the entire PR.
+
+
+
+
+ Context: User asks if their implementation follows the guidelines after making changes.
+
+ user: "Does my implementation of the retention policy differ follow the established patterns?"
+
+ assistant: "I'll use the pr-code-reviewer agent to analyze your changes and verify they align with the architectural patterns and coding standards defined in CLAUDE.md."
+
+
+ The user is seeking validation of their implementation. Use pr-code-reviewer to provide detailed feedback on pattern compliance.
+
+
tools: Bash, Glob, Grep, Read, WebSearch, AskUserQuestion
model: sonnet
color: cyan
diff --git a/.claude/agents/test-coverage-planner.md b/.claude/agents/test-coverage-planner.md
index d91b257..472db54 100644
--- a/.claude/agents/test-coverage-planner.md
+++ b/.claude/agents/test-coverage-planner.md
@@ -1,6 +1,34 @@
---
name: test-coverage-planner
-description: Use this agent when you need to analyze test coverage and create a comprehensive testing strategy for the CmdScale.EntityFrameworkCore.TimescaleDB and CmdScale.EntityFrameworkCore.TimescaleDB.Design packages. This agent should be used:\n\n1. **After implementing new features** - Example:\n - user: "I've just finished implementing the compression policy feature"\n - assistant: "Let me use the test-coverage-planner agent to analyze what tests are needed for this new feature"\n - \n\n2. **After bug fixes** - Example:\n - user: "Fixed the issue with continuous aggregate diffing"\n - assistant: "I'll use the test-coverage-planner agent to ensure we have regression tests and full coverage for this fix"\n - \n\n3. **Before releases** - Example:\n - user: "We're preparing for the 2.0 release"\n - assistant: "Let me launch the test-coverage-planner agent to verify our test coverage is comprehensive before release"\n - \n\n4. **When explicitly requested** - Example:\n - user: "Can you check our test coverage?"\n - assistant: "I'll use the test-coverage-planner agent to analyze coverage and create a testing plan"\n - \n\n5. **Proactively during development cycles** - Example:\n - user: "What should we work on next?"\n - assistant: "Let me use the test-coverage-planner agent to identify any coverage gaps that need attention"\n - \n\nThis agent focuses ONLY on planning and does NOT write or implement any tests. It produces a detailed testing strategy document that other agents (like test-writer) can use to implement the actual tests.
+description: |-
+ Use this agent when you need to analyze test coverage and create a comprehensive testing strategy for the CmdScale.EntityFrameworkCore.TimescaleDB and CmdScale.EntityFrameworkCore.TimescaleDB.Design packages. This agent should be used:
+
+ 1. **After implementing new features** - Example:
+ - user: "I've just finished implementing the compression policy feature"
+ - assistant: "Let me use the test-coverage-planner agent to analyze what tests are needed for this new feature"
+ -
+
+ 2. **After bug fixes** - Example:
+ - user: "Fixed the issue with continuous aggregate diffing"
+ - assistant: "I'll use the test-coverage-planner agent to ensure we have regression tests and full coverage for this fix"
+ -
+
+ 3. **Before releases** - Example:
+ - user: "We're preparing for the 2.0 release"
+ - assistant: "Let me launch the test-coverage-planner agent to verify our test coverage is comprehensive before release"
+ -
+
+ 4. **When explicitly requested** - Example:
+ - user: "Can you check our test coverage?"
+ - assistant: "I'll use the test-coverage-planner agent to analyze coverage and create a testing plan"
+ -
+
+ 5. **Proactively during development cycles** - Example:
+ - user: "What should we work on next?"
+ - assistant: "Let me use the test-coverage-planner agent to identify any coverage gaps that need attention"
+ -
+
+ This agent focuses ONLY on planning and does NOT write or implement any tests. It produces a detailed testing strategy document that other agents (like test-writer) can use to implement the actual tests.
tools: Bash, Glob, Grep, Read, WebSearch, AskUserQuestion
model: sonnet
color: green
diff --git a/.claude/agents/test-writer.md b/.claude/agents/test-writer.md
index 5109e76..287c88b 100644
--- a/.claude/agents/test-writer.md
+++ b/.claude/agents/test-writer.md
@@ -1,6 +1,35 @@
---
name: test-writer
-description: Use this agent when the user requests help writing, updating, or creating unit tests or integration tests for the CmdScale.EntityFrameworkCore.TimescaleDB.Tests project. This includes:\n\n\nContext: User has just implemented a new feature for continuous aggregate compression policies and wants tests for it.\nuser: "I've added a new compression policy feature for continuous aggregates. Can you write tests for the CompressionPolicyDiffer class?"\nassistant: "I'll use the test-writer agent to create comprehensive unit tests for the CompressionPolicyDiffer class."\n\n\n\n\nContext: User has completed implementing a new reorder policy feature and wants to verify it works end-to-end.\nuser: "I've finished the reorder policy implementation. Let's add some integration tests to make sure migrations work correctly."\nassistant: "I'll launch the test-writer agent to create integration tests using Testcontainers to verify the reorder policy migrations work end-to-end."\n\n\n\n\nContext: User has just written code for a new hypertable differ and wants comprehensive test coverage.\nuser: "Here's the new HypertableDiffer implementation. I need tests for all the edge cases."\nassistant: "I'll use the test-writer agent to write comprehensive unit tests covering all edge cases for the HypertableDiffer."\n\n\n\n\nContext: Proactive use - assistant detects that new code was written without tests.\nuser: "I've implemented the new ContinuousAggregateDiffer class"\nassistant: "Great work on the implementation! Now let me use the test-writer agent to create comprehensive tests for this new class to ensure it works correctly."\n\n
+description: |-
+ Use this agent when the user requests help writing, updating, or creating unit tests or integration tests for the CmdScale.EntityFrameworkCore.TimescaleDB.Tests project. This includes:
+
+
+ Context: User has just implemented a new feature for continuous aggregate compression policies and wants tests for it.
+ user: "I've added a new compression policy feature for continuous aggregates. Can you write tests for the CompressionPolicyDiffer class?"
+ assistant: "I'll use the test-writer agent to create comprehensive unit tests for the CompressionPolicyDiffer class."
+
+
+
+
+ Context: User has completed implementing a new reorder policy feature and wants to verify it works end-to-end.
+ user: "I've finished the reorder policy implementation. Let's add some integration tests to make sure migrations work correctly."
+ assistant: "I'll launch the test-writer agent to create integration tests using Testcontainers to verify the reorder policy migrations work end-to-end."
+
+
+
+
+ Context: User has just written code for a new hypertable differ and wants comprehensive test coverage.
+ user: "Here's the new HypertableDiffer implementation. I need tests for all the edge cases."
+ assistant: "I'll use the test-writer agent to write comprehensive unit tests covering all edge cases for the HypertableDiffer."
+
+
+
+
+ Context: Proactive use - assistant detects that new code was written without tests.
+ user: "I've implemented the new ContinuousAggregateDiffer class"
+ assistant: "Great work on the implementation! Now let me use the test-writer agent to create comprehensive tests for this new class to ensure it works correctly."
+
+
model: sonnet
color: green
---
diff --git a/.claude/reference/architecture.md b/.claude/reference/architecture.md
index 0ef9f76..002e509 100644
--- a/.claude/reference/architecture.md
+++ b/.claude/reference/architecture.md
@@ -42,40 +42,51 @@ This document provides detailed architectural information for the CmdScale.Entit
> When adding new features, follow the same directory structure pattern.
-#### Hypertable/ (4 files)
+#### Hypertable/ (5 files)
- `HypertableAttribute.cs` - Data annotation: `[Hypertable("TimeColumn", ChunkTimeInterval = "1 day")]`
-- `HypertableConvention.cs` - IEntityTypeAddedConvention implementation
+- `DimensionAttribute.cs` - Data annotation for additional partitioning dimensions: `[Dimension("Col", EDimensionType.Range, "1 month")]`
+- `HypertableConvention.cs` - IEntityTypeAddedConvention implementation; processes both `[Hypertable]` and `[Dimension]` attributes
- `HypertableAnnotations.cs` - Annotation constants
-- `HypertableTypeBuilder.cs` - Fluent API: `IsHypertable()`, `WithChunkTimeInterval()`, etc.
+- `HypertableTypeBuilder.cs` - Fluent API: `IsHypertable()`, `WithChunkTimeInterval()`, `HasRangeDimension()`, `HasHashDimension()`, etc.
-#### ReorderPolicy/ (3 files)
+#### ReorderPolicy/ (5 files)
- `ReorderPolicyAttribute.cs` - Data annotation: `[ReorderPolicy("index_name")]`
- `ReorderPolicyConvention.cs` - IEntityTypeAddedConvention implementation
- `ReorderPolicyAnnotations.cs` - Annotation constants
-- `ReorderPolicyTypeBuilder.cs` - Fluent API: `WithReorderPolicy()`
+- `ReorderPolicyTypeBuilder.cs` - Fluent API: `WithReorderPolicy()`; includes a scaffold-targeting overload that takes 5 positional string/int parameters and returns `ReorderPolicyStringBuilder`; also provides a chained overload on `RetentionPolicyStringBuilder` for co-located policy configuration
+- `ReorderPolicyStringBuilder.cs` - String-based builder used by scaffolded `OnModelCreating` code; exposes `WithInitialStart(DateTime)` as a chained method (DateTime cannot be rendered as a positional literal via `MethodCallCodeFragment`)
-#### RetentionPolicy/ (4 files)
+#### RetentionPolicy/ (5 files)
- `RetentionPolicyAttribute.cs` - Data annotation: `[RetentionPolicy(DropAfter = "30 days")]`
- `RetentionPolicyConvention.cs` - IEntityTypeAddedConvention implementation
- `RetentionPolicyAnnotations.cs` - Annotation constants
-- `RetentionPolicyTypeBuilder.cs` - Fluent API: `WithRetentionPolicy()`
+- `RetentionPolicyTypeBuilder.cs` - Fluent API: `WithRetentionPolicy()`; includes a scaffold-targeting overload that takes 6 positional string parameters and returns `RetentionPolicyStringBuilder`
+- `RetentionPolicyStringBuilder.cs` - String-based builder used by scaffolded `OnModelCreating` code; exposes `WithInitialStart(DateTime)` as a chained method (DateTime cannot be rendered as a positional literal via `MethodCallCodeFragment`)
-#### ContinuousAggregate/ (8 files)
+#### ContinuousAggregate/ (11 files)
- `ContinuousAggregateAttribute.cs` - Entity-level attribute defining materialized view
- `TimeBucketAttribute.cs` - Property-level attribute for time bucketing
- `AggregateAttribute.cs` - Property-level attribute with `EAggregateFunction` enum
-- `ContinuousAggregateConvention.cs` - Processes all three attributes above
-- `ContinuousAggregateAnnotations.cs` - 13 annotation constants
-- `ContinuousAggregateBuilder.cs` - Type-safe generic builder
-- `ContinuousAggregateTypeBuilder.cs` - Fluent API extensions
-
-#### ContinuousAggregatePolicy/ (5 files)
+- `GroupByColumnAttribute.cs` - Property-level attribute marking a property as a GROUP BY column
+- `ContinuousAggregateConvention.cs` - Processes all attributes above
+- `ContinuousAggregateAnnotations.cs` - Annotation constants
+- `ContinuousAggregateBuilder.cs` - Type-safe generic builder for code-first configuration
+- `ContinuousAggregateStringBuilder.cs` - String-based builder used by scaffolded `OnModelCreating` code
+- `ContinuousAggregateBuilderCore.cs` - Internal shared annotation-writing logic for both builder types
+- `ContinuousAggregateTypeBuilder.cs` - Fluent API extensions (`IsContinuousAggregate`)
+
+#### ContinuousAggregatePolicy/ (7 files)
- `ContinuousAggregatePolicyAttribute.cs` - Data annotation: `[ContinuousAggregatePolicy]`
- `ContinuousAggregatePolicyConvention.cs` - IEntityTypeAddedConvention implementation
- `ContinuousAggregatePolicyAnnotations.cs` - Annotation constants
-- `ContinuousAggregatePolicyBuilder.cs` - Fluent API builder
+- `ContinuousAggregatePolicyBuilder.cs` - Typed fluent API builder (code-first)
+- `ContinuousAggregatePolicyBuilderCore.cs` - Shared annotation-writing logic for both builder types (mirrors `ContinuousAggregateBuilderCore`)
+- `ContinuousAggregatePolicyStringBuilder.cs` - String-based builder used by scaffolded `OnModelCreating` code
- `ContinuousAggregateBuilderPolicyExtensions.cs` - Extension methods for builder
+#### Cross-cutting (ContinuousAggregatePolicy + ReorderPolicy + RetentionPolicy)
+- `PolicyJobBuilderCore.cs` - Shared base class providing annotation helpers for policy-job fields common to all three policy builder cores (ScheduleInterval, MaxRuntime, MaxRetries, RetryPeriod, InitialStart)
+
#### Cross-cutting
- `TimeColumnStoreTypeValidationConvention.cs` - IModelFinalizedConvention validating that hypertable and continuous-aggregate time columns resolve to a PostgreSQL time-dimension store type (timestamp/timestamptz/date/integer); backed by `Internals/TimeColumnStoreTypeValidator.cs`
@@ -143,6 +154,7 @@ Generated migrations call strongly-typed extension methods that construct a `Mig
- `TimescaleMigrationsModelDiffer.cs` - Extends EF Core's MigrationsModelDiffer; orchestrates the feature differs, builds the `FeatureDiffContext`, implements `GetOperationPriority()`
- `Features/IFeatureDiffer.cs` - Interface: `GetDifferences(IRelationalModel? source, IRelationalModel? target, FeatureDiffContext? context = null)`
- `Features/FeatureDiffContext.cs` - Cross-cutting diff state passed to every feature differ
+- `ParentEntityTypeResolver.cs` - Resolves a continuous aggregate's parent `IEntityType` by matching CLR class name, EF Core short name, or database table name; handles both code-first and scaffolded models
**Feature-specific:**
- `Features/Hypertables/` - `HypertableDiffer.cs`, `HypertableModelExtractor.cs`
@@ -177,6 +189,8 @@ ReorderPolicyMaxRuntime = "00:00:00" // no limit
- Registers:
- `ICSharpMigrationOperationGenerator` → `TimescaleCSharpMigrationOperationGenerator`
- `IDatabaseModelFactory` → `TimescaleDatabaseModelFactory`
+ - `IAnnotationCodeGenerator` → `TimescaleDbAnnotationCodeGenerator`
+ - `IModelCodeGeneratorSelector` → `TimescaleModelCodeGeneratorSelector`
### TimescaleCSharpMigrationOperationGenerator.cs
@@ -186,6 +200,8 @@ ReorderPolicyMaxRuntime = "00:00:00" // no limit
### Generators/ - Design-Time C# Generation
+#### Migration code generation
+
Each `*CSharpGenerator.Generate(XxxOperation, IndentedStringBuilder)` emits one typed `migrationBuilder` call, with one named argument per line.
| File | Purpose |
@@ -198,24 +214,54 @@ Each `*CSharpGenerator.Generate(XxxOperation, IndentedStringBuilder)` emits one
| `MigrationCallWriter.cs` | `IDisposable` helper that writes a `.Method(` call and named `arg: value` lines |
| `CSharpGeneratorHelper.cs` | `LiteralStringList()` for `["a", "b"]` collection expressions and `StaticCall()` for `Type.Method(args)` literals |
-### TimescaleDatabaseModelFactory.cs
+#### Annotation code generation (scaffolding phase 2)
-Orchestrates db-first scaffolding with extractor/applier pairs:
-- `HypertableScaffoldingExtractor` + `HypertableAnnotationApplier`
+Converts `DatabaseModel` annotations to C# fluent API calls or data annotation attributes in scaffolded entity files.
+
+| File | Purpose |
+|------|---------|
+| `TimescaleModelCodeGeneratorSelector.cs` | Selects `TimescaleCSharpModelGenerator` over EF Core's default `CSharpModelGenerator` |
+| `TimescaleCSharpModelGenerator.cs` | Wraps base model generator; injects TimescaleDB `using` directives when `UseDataAnnotations = true` |
+| `TimescaleDbAnnotationCodeGenerator.cs` | `IAnnotationCodeGenerator` implementation; dispatches to `IFeatureAnnotationRenderer` instances |
+| `TimescaleCSharpHelper.cs` | Extends `ICSharpHelper.UnknownLiteral` to render `NameOfCodeFragment` and mixed `object?[]` arrays |
+| `AnnotationRenderers/IFeatureAnnotationRenderer.cs` | Per-feature renderer interface: `GenerateFluentApiCalls` + `GenerateDataAnnotationAttributes` |
+| `AnnotationRenderers/HypertableAnnotationRenderer.cs` | Renders hypertable and dimension annotations to fluent API or data annotation attributes |
+| `AnnotationRenderers/ContinuousAggregateAnnotationRenderer.cs` | Renders continuous aggregate annotations; parses the stored view definition via `ViewDefinitionParser` to reconstruct structured configuration |
+| `AnnotationRenderers/ContinuousAggregatePolicyAnnotationRenderer.cs` | Renders continuous aggregate policy annotations to `WithRefreshPolicy(...)` fluent API or `[ContinuousAggregatePolicy]` attribute |
+| `AnnotationRenderers/RetentionPolicyAnnotationRenderer.cs` | Renders retention policy annotations to `WithRetentionPolicy(...)` fluent API or `[RetentionPolicy]` attribute; `ShouldRender` guard requires the parent renderer (hypertable or continuous aggregate) to have already consumed its annotation |
+| `AnnotationRenderers/ReorderPolicyAnnotationRenderer.cs` | Renders reorder policy annotations to `WithReorderPolicy(...)` fluent API or `[ReorderPolicy]` attribute; `ShouldRender` guard requires the hypertable renderer to have already consumed its annotation |
+| `AnnotationRenderers/PolicyJobRendererHelper.cs` | Shared static helpers for rendering optional policy-job fields (InitialStart, ScheduleInterval, MaxRuntime, etc.) shared across all policy renderers |
+| `AnnotationRenderers/AnnotationRendererHelper.cs` | Static helpers: `Find`, `GetString`, `SplitColumns`, `Consume`, `ResolvePropertyName`, `TryResolvePropertyName`, `ResolveColumns` |
+| `AnnotationRenderers/NameOfCodeFragment.cs` | Custom `CodeFragment` record: renders as `nameof(Property)` or `$"{nameof(Property)} DESC"` |
+
+### Scaffolding Pipeline
+
+`dotnet ef dbcontext scaffold` runs in two phases:
+
+**Phase 1 — Database extraction** (`TimescaleDatabaseModelFactory.cs` + `Scaffolding/`):
+`TimescaleDatabaseModelFactory` overrides NpgsqlDatabaseModelFactory. After the base factory builds the `DatabaseModel` from the database schema, it calls each extractor/applier pair to layer TimescaleDB metadata on top as annotations. All interval fields are normalized to humanized units (e.g. `"1 hour"`) via `IntervalParsingHelper.NormalizeInterval` to avoid phantom migrations from PostgreSQL's `HH:MM:SS` rendering:
+- `HypertableScaffoldingExtractor` + `HypertableAnnotationApplier` — hypertable config, dimensions, chunk time interval
- `ReorderPolicyScaffoldingExtractor` + `ReorderPolicyAnnotationApplier`
+- `RetentionPolicyScaffoldingExtractor` + `RetentionPolicyAnnotationApplier`
- `ContinuousAggregateScaffoldingExtractor` + `ContinuousAggregateAnnotationApplier`
+- `ContinuousAggregatePolicyScaffoldingExtractor` + `ContinuousAggregatePolicyAnnotationApplier`
+
+**Phase 2 — Annotation code generation** (`TimescaleDbAnnotationCodeGenerator` + `AnnotationRenderers/`):
+EF Core's scaffolding pipeline calls `TimescaleDbAnnotationCodeGenerator` to convert those annotations into C# code. The dispatcher iterates its registered `IFeatureAnnotationRenderer` implementations:
+- When `UseDataAnnotations = false` → `GenerateFluentApiCalls` → fluent API method chains in `OnModelCreating`
+- When `UseDataAnnotations = true` → `GenerateDataAnnotationAttributes` → `[Attribute]` declarations on entity classes
+
+Registered renderers: `HypertableAnnotationRenderer`, `ContinuousAggregateAnnotationRenderer`, `ContinuousAggregatePolicyAnnotationRenderer`, `RetentionPolicyAnnotationRenderer`, `ReorderPolicyAnnotationRenderer`. Registration order matters: child renderers (`ContinuousAggregatePolicyAnnotationRenderer`, `RetentionPolicyAnnotationRenderer`, `ReorderPolicyAnnotationRenderer`) must run after their respective parent renderers so the `ShouldRender` guard can verify the parent annotation was consumed.
-### Scaffolding/
+`TimescaleCSharpModelGenerator` wraps EF Core's standard model generator and post-processes the generated files to inject missing `using` directives for TimescaleDB attribute namespaces. `TimescaleModelCodeGeneratorSelector` ensures this custom generator is selected.
-**Interfaces:**
+**Additional Design-Time Utilities:**
+- `Scaffolding/ViewDefinitionParser.cs` - Parses a continuous aggregate's stored view definition SQL (best-effort, cached) to extract `TimeBucketWidth`, `TimeBucketSourceColumn`, aggregate functions, GROUP BY columns, and WHERE clause; used by `ContinuousAggregateAnnotationRenderer`
+
+**Scaffolding/ Interfaces:**
- `ITimescaleFeatureExtractor.cs` - `Extract(DbConnection connection)` returns feature metadata
- `IAnnotationApplier.cs` - `ApplyAnnotations(DatabaseTable table, object featureInfo)`
-**Feature Extractors** query TimescaleDB system tables:
-- `HypertableScaffoldingExtractor.cs` - Queries `timescaledb_information.hypertables`, dimensions, chunk stats
-- `ReorderPolicyScaffoldingExtractor.cs` - Queries `timescaledb_information.jobs`
-- `ContinuousAggregateScaffoldingExtractor.cs` - Queries continuous aggregate metadata
-
### build/CmdScale.EntityFrameworkCore.TimescaleDB.Design.targets
- MSBuild integration that injects DesignTimeServicesReference attribute
diff --git a/.claude/reference/file-organization.md b/.claude/reference/file-organization.md
index 98e31c7..5a28479 100644
--- a/.claude/reference/file-organization.md
+++ b/.claude/reference/file-organization.md
@@ -21,6 +21,7 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| `Configuration/Hypertable/HypertableTypeBuilder.cs` | Fluent API |
| `Configuration/Hypertable/HypertableAnnotations.cs` | Annotation constants |
| `Configuration/Hypertable/HypertableAttribute.cs` | Data annotation |
+| `Configuration/Hypertable/DimensionAttribute.cs` | Data annotation for additional partitioning dimensions |
| `Configuration/Hypertable/HypertableConvention.cs` | Convention processing |
| `Internals/Features/Hypertables/HypertableDiffer.cs` | Diffing logic |
| `Internals/Features/Hypertables/HypertableModelExtractor.cs` | Model extraction |
@@ -33,7 +34,8 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| File | Purpose |
|------|---------|
-| `Configuration/ReorderPolicy/ReorderPolicyTypeBuilder.cs` | Fluent API |
+| `Configuration/ReorderPolicy/ReorderPolicyTypeBuilder.cs` | Fluent API (including scaffold-targeting overload) |
+| `Configuration/ReorderPolicy/ReorderPolicyStringBuilder.cs` | String-based builder used in scaffolded code |
| `Configuration/ReorderPolicy/ReorderPolicyAnnotations.cs` | Annotation constants |
| `Configuration/ReorderPolicy/ReorderPolicyAttribute.cs` | Data annotation |
| `Configuration/ReorderPolicy/ReorderPolicyConvention.cs` | Convention processing |
@@ -49,7 +51,8 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| File | Purpose |
|------|---------|
-| `Configuration/RetentionPolicy/RetentionPolicyTypeBuilder.cs` | Fluent API |
+| `Configuration/RetentionPolicy/RetentionPolicyTypeBuilder.cs` | Fluent API (including scaffold-targeting overload) |
+| `Configuration/RetentionPolicy/RetentionPolicyStringBuilder.cs` | String-based builder used in scaffolded code |
| `Configuration/RetentionPolicy/RetentionPolicyAnnotations.cs` | Annotation constants |
| `Configuration/RetentionPolicy/RetentionPolicyAttribute.cs` | Data annotation |
| `Configuration/RetentionPolicy/RetentionPolicyConvention.cs` | Convention processing |
@@ -65,15 +68,19 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| File | Purpose |
|------|---------|
-| `Configuration/ContinuousAggregate/ContinuousAggregateBuilder.cs` | Type-safe builder |
+| `Configuration/ContinuousAggregate/ContinuousAggregateBuilder.cs` | Type-safe generic builder |
+| `Configuration/ContinuousAggregate/ContinuousAggregateBuilderCore.cs` | Shared annotation-writing logic for both builder types |
+| `Configuration/ContinuousAggregate/ContinuousAggregateStringBuilder.cs` | String-based builder used in scaffolded code |
| `Configuration/ContinuousAggregate/ContinuousAggregateTypeBuilder.cs` | Fluent API extensions |
| `Configuration/ContinuousAggregate/ContinuousAggregateAnnotations.cs` | Annotation constants |
| `Configuration/ContinuousAggregate/ContinuousAggregateAttribute.cs` | Entity-level attribute |
| `Configuration/ContinuousAggregate/TimeBucketAttribute.cs` | Property-level attribute |
| `Configuration/ContinuousAggregate/AggregateAttribute.cs` | Property-level attribute |
+| `Configuration/ContinuousAggregate/GroupByColumnAttribute.cs` | Property-level attribute for GROUP BY columns |
| `Configuration/ContinuousAggregate/ContinuousAggregateConvention.cs` | Convention processing |
| `Internals/Features/ContinuousAggregates/ContinuousAggregateDiffer.cs` | Diffing logic |
| `Internals/Features/ContinuousAggregates/ContinuousAggregateModelExtractor.cs` | Model extraction |
+| `Internals/ParentEntityTypeResolver.cs` | Resolves a continuous aggregate's parent entity type by CLR name, EF short name, or table name |
| `Generators/ContinuousAggregateSqlGenerator.cs` | Runtime SQL generation |
| `MigrationExtensions/ContinuousAggregateMigrationExtensions.cs` | Typed migrationBuilder methods |
| `Abstractions/ContinuousAggregateFunction.cs` | Typed aggregate-function value |
@@ -88,8 +95,11 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyAnnotations.cs` | Annotation constants |
| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyAttribute.cs` | Data annotation |
| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyConvention.cs` | Convention processing |
-| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyBuilder.cs` | Fluent API builder |
+| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyBuilder.cs` | Typed fluent API builder (code-first) |
+| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyBuilderCore.cs` | Shared annotation-writing logic for both builder types |
+| `Configuration/ContinuousAggregatePolicy/ContinuousAggregatePolicyStringBuilder.cs` | String-based builder used in scaffolded code |
| `Configuration/ContinuousAggregatePolicy/ContinuousAggregateBuilderPolicyExtensions.cs` | Builder extensions |
+| `Configuration/PolicyJobBuilderCore.cs` | Shared base class for policy job builder cores (reorder, retention, CA policy) |
| `Internals/Features/ContinuousAggregatePolicies/ContinuousAggregatePolicyDiffer.cs` | Diffing logic |
| `Internals/Features/ContinuousAggregatePolicies/ContinuousAggregatePolicyModelExtractor.cs` | Model extraction |
| `Generators/ContinuousAggregatePolicySqlGenerator.cs` | Runtime SQL generation |
@@ -124,6 +134,8 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
## Design Library Key Files
+### Entry Points & Migration Code Generation
+
| File | Purpose |
|------|---------|
| `TimescaleDBDesignTimeServices.cs` | Register design-time services |
@@ -135,6 +147,29 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| `Generators/ContinuousAggregatePolicyCSharpGenerator.cs` | Emits CA-policy calls |
| `Generators/MigrationCallWriter.cs` | Writes a `.Method(arg: value, …)` call |
| `Generators/CSharpGeneratorHelper.cs` | Collection-expression and static-call literal helpers |
+
+### Annotation Code Generation (Scaffolding Phase 2)
+
+| File | Purpose |
+|------|---------|
+| `Generators/TimescaleModelCodeGeneratorSelector.cs` | Prefers `TimescaleCSharpModelGenerator` over base `CSharpModelGenerator` |
+| `Generators/TimescaleCSharpModelGenerator.cs` | Injects TimescaleDB `using` directives when `UseDataAnnotations = true` |
+| `Generators/TimescaleDbAnnotationCodeGenerator.cs` | Dispatches to `IFeatureAnnotationRenderer` implementations |
+| `Generators/TimescaleCSharpHelper.cs` | Extends `ICSharpHelper.UnknownLiteral` for `NameOfCodeFragment` and mixed arrays |
+| `Generators/AnnotationRenderers/IFeatureAnnotationRenderer.cs` | Per-feature renderer interface |
+| `Generators/AnnotationRenderers/HypertableAnnotationRenderer.cs` | Renders hypertable annotations to fluent API or data annotation C# |
+| `Generators/AnnotationRenderers/ContinuousAggregateAnnotationRenderer.cs` | Renders continuous aggregate annotations by parsing the view definition |
+| `Generators/AnnotationRenderers/ContinuousAggregatePolicyAnnotationRenderer.cs` | Renders continuous aggregate policy annotations to `WithRefreshPolicy(...)` fluent API or `[ContinuousAggregatePolicy]` attribute |
+| `Generators/AnnotationRenderers/RetentionPolicyAnnotationRenderer.cs` | Renders retention policy annotations to `WithRetentionPolicy(...)` fluent API or `[RetentionPolicy]` attribute; registered after parent renderers (hypertable and continuous aggregate) |
+| `Generators/AnnotationRenderers/ReorderPolicyAnnotationRenderer.cs` | Renders reorder policy annotations to `WithReorderPolicy(...)` fluent API or `[ReorderPolicy]` attribute; registered after the hypertable renderer |
+| `Generators/AnnotationRenderers/PolicyJobRendererHelper.cs` | Shared helpers for emitting policy-job optional arguments (`InitialStart`, `WithScheduleInterval`, etc.) |
+| `Generators/AnnotationRenderers/AnnotationRendererHelper.cs` | Static helpers: `Find`, `GetString`, `SplitColumns`, `Consume`, `ResolvePropertyName`, `TryResolvePropertyName` |
+| `Generators/AnnotationRenderers/NameOfCodeFragment.cs` | Custom `CodeFragment` producing `nameof(X)` or `$"{nameof(X)} DESC"` |
+
+### Scaffolding (Phase 1: Database Extraction)
+
+| File | Purpose |
+|------|---------|
| `TimescaleDatabaseModelFactory.cs` | Db-first scaffolding orchestration |
| `Scaffolding/ITimescaleFeatureExtractor.cs` | Extractor interface |
| `Scaffolding/IAnnotationApplier.cs` | Applier interface |
@@ -146,6 +181,10 @@ Quick reference for locating key files in the CmdScale.EntityFrameworkCore.Times
| `Scaffolding/RetentionPolicyAnnotationApplier.cs` | Apply retention policy annotations |
| `Scaffolding/ContinuousAggregateScaffoldingExtractor.cs` | Query continuous aggregates |
| `Scaffolding/ContinuousAggregateAnnotationApplier.cs` | Apply continuous aggregate annotations |
+| `Scaffolding/ContinuousAggregatePolicyScaffoldingExtractor.cs` | Query continuous aggregate refresh policies from database |
+| `Scaffolding/ContinuousAggregatePolicyAnnotationApplier.cs` | Apply continuous aggregate policy annotations |
+| `Scaffolding/IntervalParsingHelper.cs` | Parses and normalizes PostgreSQL interval strings (e.g. `"01:00:00"` → `"1 hour"`) and integer offsets |
+| `Scaffolding/ViewDefinitionParser.cs` | Parses continuous aggregate view SQL to extract structured configuration for code generation |
| `build/CmdScale.EntityFrameworkCore.TimescaleDB.Design.targets` | MSBuild integration |
## Test Files
@@ -190,8 +229,9 @@ src/
│ └── *.cs # Entry points, extensions
│
└── Eftdb.Design/ # Design-time library (CmdScale.EntityFrameworkCore.TimescaleDB.Design)
- ├── Generators/ # Design-time C# (typed migration call) generation
- ├── Scaffolding/ # Extractors and appliers
+ ├── Generators/ # Design-time C# generation (migration calls + scaffolding code)
+ │ └── AnnotationRenderers/ # Per-feature annotation-to-C# renderers
+ ├── Scaffolding/ # Database extractors and annotation appliers
├── build/ # MSBuild targets
└── *.cs # Design-time services
diff --git a/.claude/reference/patterns.md b/.claude/reference/patterns.md
index 24b1060..1e83b68 100644
--- a/.claude/reference/patterns.md
+++ b/.claude/reference/patterns.md
@@ -211,10 +211,48 @@ Keep each class focused on a single responsibility:
| Runtime SQL | Convert operations to SQL | `Generators/*SqlGenerator` classes |
| Design-time C# | Convert operations to typed migration calls | `Design/Generators/*CSharpGenerator` classes |
| Migration API | Construct operations from migration files | `MigrationExtensions/*MigrationExtensions` classes |
-| Scaffolding | Reverse engineer from database | Scaffolding extractors/appliers |
+| Scaffolding extraction | Reverse engineer from database | `Scaffolding/*ScaffoldingExtractor` + `*AnnotationApplier` classes |
+| Scaffolding code generation | Annotations → C# fluent API or data annotation attributes | `Design/Generators/AnnotationRenderers/*AnnotationRenderer` classes |
**Never mix concerns:** Extractors should not generate SQL, differs should not read databases.
+## 13. Scaffolding Annotation Code Generation
+
+`dotnet ef dbcontext scaffold` runs two distinct phases:
+
+**Phase 1 — Database extraction** (`Scaffolding/`): `TimescaleDatabaseModelFactory` calls each `*ScaffoldingExtractor` to query TimescaleDB system tables, then calls the matching `*AnnotationApplier` to store the metadata as annotations on the EF Core `DatabaseModel`. The result is the same annotation format the runtime library uses.
+
+**Phase 2 — Code generation** (`Generators/AnnotationRenderers/`): EF Core's scaffolding pipeline asks `TimescaleDbAnnotationCodeGenerator` to convert those annotations into C# code. It dispatches to registered `IFeatureAnnotationRenderer` implementations.
+
+**`IFeatureAnnotationRenderer` contract:**
+
+```csharp
+interface IFeatureAnnotationRenderer
+{
+ // Called when UseDataAnnotations = false — emit fluent API calls
+ void GenerateFluentApiCalls(
+ IEntityType entityType,
+ Dictionary annotations,
+ CSharpRuntimeAnnotationCodeGeneratorParameters parameters);
+
+ // Called when UseDataAnnotations = true — return attribute fragments
+ IReadOnlyList GenerateDataAnnotationAttributes(
+ IEntityType entityType,
+ Dictionary annotations);
+}
+```
+
+**Key rules:**
+- Call `AnnotationRendererHelper.Consume(annotations, keys...)` for every annotation key you handle. Unconsumed annotations cause EF Core to emit a raw `.HasAnnotation("key", value)` fallback in the scaffolded code.
+- Use `AnnotationRendererHelper.ResolvePropertyName(entityType, columnName)` to map database column names back to C# property names.
+- Use `NameOfCodeFragment` to emit `nameof(Entity.Property)` instead of hard-coded string literals so the scaffolded code is refactoring-safe. `TimescaleCSharpHelper.UnknownLiteral` handles rendering these.
+- Register each new renderer in `TimescaleDbAnnotationCodeGenerator`.
+- If a renderer emits attributes from a new namespace, add that namespace to `TimescaleCSharpModelGenerator.CollectAttributeNamespaces()` so the `using` directive is injected automatically.
+
+**`TimescaleCSharpModelGenerator`** sits at the top of the scaffolding code generation chain. It wraps EF Core's standard `CSharpModelGenerator` and, when `UseDataAnnotations = true`, inspects the generated entity files to add any missing TimescaleDB `using` directives. `TimescaleModelCodeGeneratorSelector` ensures this generator is selected in preference to EF Core's default `CSharpModelGenerator`.
+
+**Location:** `Design/Generators/AnnotationRenderers/`, `Design/Generators/TimescaleDbAnnotationCodeGenerator.cs`, `Design/Generators/TimescaleCSharpModelGenerator.cs`, `Design/Generators/TimescaleModelCodeGeneratorSelector.cs`
+
```csharp
// Correct - Separation of concerns
public class HypertableDiffer : IFeatureDiffer
diff --git a/.claude/skills/prepare-commit/SKILL.md b/.claude/skills/prepare-commit/SKILL.md
index 84e46ca..8ffb506 100644
--- a/.claude/skills/prepare-commit/SKILL.md
+++ b/.claude/skills/prepare-commit/SKILL.md
@@ -1,6 +1,6 @@
---
name: prepare-commit
-description: Prepare changes for commit. Formats code, runs tests, updates READMEs, stages files, and generates a commit message for review.
+description: Prepare changes for commit. Formats code, runs tests, updates READMEs, and generates a commit message for review. Does not stage files.
user-invocable: true
---
@@ -18,14 +18,13 @@ Pass the full context of what needs to be done.
3. Run `dotnet test` to verify all tests pass
4. If files were added/removed/renamed in `src/`, update `.claude/reference/file-organization.md` and `.claude/reference/architecture.md`
5. Update relevant README.md files if features or APIs changed
-6. Stage relevant files with `git add` (specific files, not `-A`)
-7. Generate a conventional commit message based on the staged changes
+6. Generate a conventional commit message based on the working tree changes
## Rules
- **NEVER** execute `git commit` — the user reviews and commits manually
+- **NEVER** stage changes — do not run `git add` in any form; the user stages files themselves so the working tree stays easy to review
- **NEVER** push to remote
-- Use specific file paths with `git add`, never `git add -A` or `git add .`
-- Skip files that likely contain secrets (`.env`, credentials)
+- Skip inspecting files that likely contain secrets (`.env`, credentials)
- Follow the repository's existing commit message style (check `git log`)
- Use conventional commits if you can infer the type (feat, fix, docs, etc.) from the changes and you think it would be helpful for the user to see that in the message. Note that conventional commits will be added to the release notes by the generate-changelog.yml workflow, so they should be used when the commit represents a meaningful change that should be highlighted in the changelog. However, if the changes are minor or don't fit well into a conventional commit type, it's better to write a clear, descriptive message without forcing a conventional format.
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index c070400..67419b6 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -33,11 +33,11 @@ jobs:
run: dotnet build --no-restore
- name: Run tests with coverage
- run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
+ run: dotnet test --no-build --verbosity normal --settings tests/Eftdb.Tests/coverlet.runsettings --collect:"XPlat Code Coverage" --results-directory ./coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: ./coverage
+ files: ./coverage/**/coverage.cobertura.xml
fail_ci_if_error: true
\ No newline at end of file
diff --git a/docs/01-dotnet-tools.md b/docs/01-dotnet-tools.md
index 87c55dc..13e9588 100644
--- a/docs/01-dotnet-tools.md
+++ b/docs/01-dotnet-tools.md
@@ -109,7 +109,7 @@ This command will:
### Example
-The scaffolding process generates data annotations that represent the hypertable's configuration in the database. This allows your `DbContext` to be aware of the TimescaleDB features without manual configuration.
+The scaffolding process reconstructs the library's Fluent API configuration from the database. This allows your `DbContext` to be aware of the TimescaleDB features without manual configuration.
```csharp
modelBuilder.HasPostgresExtension("timescaledb");
@@ -117,16 +117,30 @@ modelBuilder.Entity(entity =>
{
entity.HasKey(e => new { e.Id, e.Time });
- entity
- .HasAnnotation("TimescaleDB:ChunkSkipColumns", "Time")
- .HasAnnotation("TimescaleDB:ChunkTimeInterval", "86400000")
- .HasAnnotation("TimescaleDB:EnableCompression", true)
- .HasAnnotation("TimescaleDB:HasReorderPolicy", true)
- .HasAnnotation("TimescaleDB:IsHypertable", true)
- .HasAnnotation("TimescaleDB:ReorderPolicy:IndexName", "DeviceReadings_Time_idx")
- .HasAnnotation("TimescaleDB:ReorderPolicy:ScheduleInterval", "12:00:00")
- .HasAnnotation("TimescaleDB:TimeColumnName", "Time");
-
- entity.HasIndex(e => e.Time, "DeviceReadings_Time_idx").IsDescending();
+ entity.IsHypertable(x => x.Time)
+ .WithChunkTimeInterval("1 day")
+ .EnableCompression()
+ .WithChunkSkipping(x => x.Time);
});
-```
\ No newline at end of file
+```
+
+With the `--data-annotations` flag, the configuration is expressed as attributes on the entity classes instead (`[Hypertable]`, `[Dimension]`, `[ContinuousAggregate]`, etc.).
+
+```bash
+dotnet ef dbcontext scaffold "" CmdScale.EntityFrameworkCore.TimescaleDB.Design --data-annotations ...
+```
+
+### Scaffolding Limitations
+
+Some configuration cannot be recovered from the database catalog, and some values come back in a normalized form. A scaffolded model is functionally equivalent to the database, but does not always textually match the hand-written configuration that created it:
+
+- **`WithNoData` and `CreateGroupIndexes`** are creation-time options of a continuous aggregate and are not queryable from the catalog. They scaffold as their defaults.
+- **Implicitly enabled compression** is reported as explicit: enabling chunk skipping requires compression, so a hypertable configured only with chunk skipping scaffolds with `EnableCompression` set.
+- **Calendar intervals are stored normalized** by TimescaleDB: a chunk interval of `"1 month"` is stored as a fixed duration and scaffolds as `"30 days"`.
+- **Interval values are only humanized when exact**: `"01:00:00"` becomes `"1 hour"`, but a value that does not reduce to a single unit (e.g. `"01:30:00"`) is kept in its raw form rather than being rounded.
+- **WHERE clauses come back PostgreSQL-normalized**, including casts and parentheses (e.g. `"ticker" = 'MCRS'` scaffolds as `(ticker = 'MCRS'::text)`).
+- **TimescaleDB's auto-created indexes are suppressed**: the descending time index (`
__idx`) and the per-dimension composites (`
___idx`) are recreated automatically by TimescaleDB and do not scaffold as explicit indexes. A user-defined, non-unique index that exactly matches this name and column pattern is indistinguishable from the auto-created one and is suppressed as well.
+- **Raw GROUP BY expressions have no data-annotations representation**: in `--data-annotations` mode, a GROUP BY entry that is not a plain column (e.g. `EXTRACT(HOUR FROM time)`) is reported as a warning and must be configured via the Fluent API's `AddGroupByColumn(...)`.
+- **Unparseable view definitions degrade gracefully**: when a continuous aggregate's view SQL cannot be parsed into the structured configuration, a warning is reported and the configuration is preserved as `.HasAnnotation(...)` calls; migrations recreate the view from the raw SQL definition.
+
+For the full normalization contract and all known limitations, see [Scaffolding Behavior and Limitations](./03-scaffolding.md).
\ No newline at end of file
diff --git a/docs/03-scaffolding.md b/docs/03-scaffolding.md
new file mode 100644
index 0000000..843425c
--- /dev/null
+++ b/docs/03-scaffolding.md
@@ -0,0 +1,45 @@
+# Scaffolding Behavior and Limitations
+
+Scaffolding a TimescaleDB database with the `CmdScale.EntityFrameworkCore.TimescaleDB.Design` package emits typed Fluent API calls or data annotations for all TimescaleDB features. The scaffolded model is functionally equivalent to the database — migrating from it produces an identical schema, a contract enforced by roundtrip integration tests. See [dotnet ef tools](./01-dotnet-tools.md) for setup and command usage.
+
+## Positional-null policy forms
+
+Scaffolded retention and reorder policy calls use all positional arguments, filling omitted parameters with `null`:
+
+```csharp
+.WithRetentionPolicy("90 days", null, null, null, 3, "15 minutes")
+.WithReorderPolicy("trade_time_idx", null, null, 2, "10 minutes")
+```
+
+This is intentional: the user-facing overloads take `initialStart` (`DateTime?`) as an early positional parameter, and a trimmed call would bind later arguments to the wrong parameters. The parameter order is:
+
+```csharp
+.WithRetentionPolicy(dropAfter, dropCreatedBefore, scheduleInterval, maxRuntime, maxRetries, retryPeriod)
+.WithReorderPolicy(indexName, scheduleInterval, maxRuntime, maxRetries, retryPeriod)
+```
+
+`InitialStart` is always chained as a separate `.WithInitialStart(DateTime)` call. Rewriting a scaffolded call to named-argument style by hand is safe — the result is annotation-equivalent.
+
+The continuous aggregate refresh policy is not affected and scaffolds with trailing nulls trimmed (`.WithRefreshPolicy("7 days", "1 hour")`).
+
+## Normalization contract
+
+Scaffolding reproduces what the database stores, which is not always the literal text the original code wrote:
+
+- **Intervals are humanized when an exact single-unit reduction exists**: `01:00:00` → `1 hour`, `86400000000` (bigint µs) → `1 day`, `2 day` → `2 days`. Values without a single-unit reduction (`01:30:00`) are kept as-is. Calendar units collapse to the fixed duration TimescaleDB stores: `1 month` → `30 days`.
+- **WHERE clauses come back PostgreSQL-normalized**: `"\"ticker\" = 'MCRS'"` scaffolds as `"(ticker = 'MCRS'::text)"`.
+- **GROUP BY is canonicalized by PostgreSQL** before scaffolding sees it: positional notation (`.AddGroupByColumn("1, 2")`) is resolved to the referenced expressions. Simple columns become typed group-by entries, the `time_bucket(...)` expression is represented by the time-bucket configuration, and non-column expressions are preserved verbatim as raw strings. Grouping semantics always survive; the positional notation does not.
+- **Values equal to TimescaleDB defaults are omitted** and filled back in by the runtime.
+- **Implicitly enabled compression is reported explicitly**: chunk skipping requires compression, so a hypertable configured only with `.WithChunkSkipping(...)` scaffolds with `EnableCompression` set.
+- **TimescaleDB's auto-created indexes are suppressed** (`
__idx` and per-dimension composites) — they are recreated automatically on hypertable creation. A user-defined, non-unique index exactly matching this name and column pattern is indistinguishable and is suppressed as well.
+- **TimescaleDB internal schemas** (`_timescaledb_internal`, `_timescaledb_catalog`, `_timescaledb_config`, `_timescaledb_cache`) are excluded automatically unless explicitly requested via `--schema`.
+
+---
+
+## Known limitations
+
+- **`WithNoData` and `CreateGroupIndexes`** are creation-time-only options of a continuous aggregate and are not queryable from the catalog. Scaffolded aggregates use the defaults (populated on creation, group indexes created). The schema is identical; only the initial-population timing differs.
+- **CLR type fidelity is not preserved**: custom mapped types (e.g. NodaTime `LocalDateTime`) scaffold as BCL types with an explicit store type (`[Column(TypeName = "timestamp without time zone")] DateTime`). The column definition is identical; re-apply the custom type mapping manually if needed.
+- **`InitialStart` sub-microsecond precision is lost**: PostgreSQL stores microseconds, so .NET's seventh fractional digit (100 ns ticks) does not survive — `…19.3905112Z` scaffolds as `…19.3905110Z`.
+- **Non-column GROUP BY expressions have no data-annotations representation**: in `--data-annotations` mode they are reported as a warning and must be configured via the Fluent API's `AddGroupByColumn(string)`.
+- **Unparseable view definitions degrade gracefully**: when a continuous aggregate's view SQL cannot be parsed into typed configuration, a warning is reported and the configuration is preserved as `.HasAnnotation(...)` calls; migrations still recreate the view correctly from the raw SQL definition.
diff --git a/docs/data-annotations/continuous-aggregates.md b/docs/data-annotations/continuous-aggregates.md
index 40fbbc4..8c5f211 100644
--- a/docs/data-annotations/continuous-aggregates.md
+++ b/docs/data-annotations/continuous-aggregates.md
@@ -244,7 +244,30 @@ public class TradeHourlyAggregate
## Grouping by Additional Columns
-Data Annotations do not support GROUP BY configuration beyond the time bucket. For grouping by additional columns from the source hypertable, use the [Fluent API](../fluent-api/continuous-aggregates#grouping-data) approach.
+Use the `[GroupByColumn]` attribute on a property of the aggregate entity to add it to the GROUP BY clause. Without an argument, the property's own name is used as the source column; pass a source column explicitly when the names differ:
+
+```csharp
+[Keyless]
+[ContinuousAggregate(
+ MaterializedViewName = "trade_hourly_stats",
+ ParentName = nameof(Trade))]
+[TimeBucket("1 hour", nameof(Trade.Timestamp))]
+public class TradeHourlyAggregate
+{
+ // Grouped by the parent's Exchange column (same name as this property).
+ [GroupByColumn]
+ public string Exchange { get; set; } = string.Empty;
+
+ // Grouped by the parent's Ticker column, exposed under a different property name.
+ [GroupByColumn(nameof(Trade.Ticker))]
+ public string Symbol { get; set; } = string.Empty;
+
+ [Aggregate(EAggregateFunction.Avg, nameof(Trade.Price))]
+ public decimal AveragePrice { get; set; }
+}
+```
+
+> :warning: **Note:** Raw SQL GROUP BY expressions (e.g. `EXTRACT(HOUR FROM time)`) cannot be expressed as an attribute. Use the [Fluent API](../fluent-api/continuous-aggregates#grouping-data)'s `AddGroupByColumn(string)` overload for those.
## Refresh Policies
diff --git a/docs/fluent-api/continuous-aggregates.md b/docs/fluent-api/continuous-aggregates.md
index c2fb5d9..869b63c 100644
--- a/docs/fluent-api/continuous-aggregates.md
+++ b/docs/fluent-api/continuous-aggregates.md
@@ -88,7 +88,7 @@ public void Configure(EntityTypeBuilder builder)
## Grouping Data
-Beyond the time bucket, continuous aggregates can group data by additional columns from the source hypertable.
+Beyond the time bucket, continuous aggregates can group data by additional columns from the source hypertable. In Data Annotations configuration, the same is expressed with the property-level [`[GroupByColumn]` attribute](../data-annotations/continuous-aggregates#grouping-by-additional-columns); raw SQL expressions are Fluent-API-only.
### Group By Column
diff --git a/samples/Eftdb.Samples.CodeFirst/TimescaleDBDesignTimeService.cs b/samples/Eftdb.Samples.CodeFirst/TimescaleDBDesignTimeService.cs
index 116343d..b88b1a6 100644
--- a/samples/Eftdb.Samples.CodeFirst/TimescaleDBDesignTimeService.cs
+++ b/samples/Eftdb.Samples.CodeFirst/TimescaleDBDesignTimeService.cs
@@ -1,4 +1,5 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Design;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.EntityFrameworkCore.Scaffolding;
@@ -16,7 +17,8 @@ public void ConfigureDesignTimeServices(IServiceCollection services)
// The reason for this is, because the CmdScale.EntityFrameworkCore.TimescaleDB.Design project only copies the required assembly-attribute when being packaged.
services.AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton();
}
}
}
diff --git a/samples/Eftdb.Samples.DatabaseFirst/README.md b/samples/Eftdb.Samples.DatabaseFirst/README.md
index 61dfe0e..f703b2b 100644
--- a/samples/Eftdb.Samples.DatabaseFirst/README.md
+++ b/samples/Eftdb.Samples.DatabaseFirst/README.md
@@ -2,24 +2,13 @@
This project demonstrates how to use the **Database-First** approach with [TimescaleDB](https://www.timescale.com/) using the `CmdScale.EntityFrameworkCore.TimescaleDB` package.
-
-> [!WARNING]
-> Currently the `dotnet ef dbcontext scaffold` command can't be tested because of an issue in `efcore` (see https://github.com/dotnet/efcore/issues/37201).
-> TODO: Test as soon as there is a fix available
+For command usage see [dotnet ef tools](../../docs/01-dotnet-tools.md); for the normalization contract and known limitations see [Scaffolding Behavior and Limitations](../../docs/03-scaffolding.md).
---
-## Required NuGet Packages
-
-Ensure the following package is installed in your project:
-
-- `CmdScale.EntityFrameworkCore.TimescaleDB.Design`
-
----
+## Quick Start
-## Scaffold DbContext and Models
-
-Use the following command to scaffold the `DbContext` and entity classes from an existing TimescaleDB database:
+Install the design-time package, then run:
```bash
dotnet ef dbcontext scaffold \
@@ -32,13 +21,9 @@ dotnet ef dbcontext scaffold \
--project samples/Eftdb.Samples.DatabaseFirst
```
-This command will:
-
-- Generate entity models in the `Models/` directory
-- Place the `MyTimescaleDbContext` in the current directory
-- Use the specified connection string to connect to the TimescaleDB instance
+Add `--data-annotations` to generate attributes instead of Fluent API calls.
-> **Note**: When scaffolding your `DbContext`, use the `--schema` flag to target only your data schema (usually `public`). This prevents the tool from unintentionally including TimescaleDB's internal management tables from schemas like `_timescaledb_internal`.
+> **Note**: Use `--schema public` to prevent the tool from including TimescaleDB's internal management schemas (`_timescaledb_internal`, etc.).
---
@@ -55,17 +40,18 @@ samples/Eftdb.Samples.DatabaseFirst/
## Docker
-- A `docker-compose.yml` file is available at the repository root to spin up a TimescaleDB container for local development:
+A `docker-compose.yml` file is available at the repository root to spin up a TimescaleDB container for local development:
- ```bash
- docker-compose up -d
- ```
+```bash
+docker-compose up -d
+```
-- Connection string settings should match the configuration in your `docker-compose.yml`.
+Connection string settings should match the configuration in your `docker-compose.yml`.
---
## Resources
+- [Scaffolding Behavior and Limitations](../../docs/03-scaffolding.md)
- [Entity Framework Core Documentation](https://learn.microsoft.com/en-us/ef/core/)
- [TimescaleDB Documentation](https://docs.timescale.com/)
diff --git a/samples/Eftdb.Samples.Shared/Configurations/OrderStatusEventConfiguration.cs b/samples/Eftdb.Samples.Shared/Configurations/OrderStatusEventConfiguration.cs
index ea74d32..681f0d0 100644
--- a/samples/Eftdb.Samples.Shared/Configurations/OrderStatusEventConfiguration.cs
+++ b/samples/Eftdb.Samples.Shared/Configurations/OrderStatusEventConfiguration.cs
@@ -1,5 +1,4 @@
-using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
-using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
using CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -15,8 +14,8 @@ public void Configure(EntityTypeBuilder builder)
builder
.IsHypertable(e => e.EventTimestamp)
.WithChunkTimeInterval("7 days")
- .HasDimension(Dimension.CreateRange("OrderPlacedTimestamp", "1 month"))
- .HasDimension(Dimension.CreateHash("WarehouseId", 4));
+ .HasRangeDimension(e => e.OrderPlacedTimestamp, "1 month")
+ .HasHashDimension(e => e.WarehouseId, 4);
}
}
}
diff --git a/samples/Eftdb.Samples.Shared/Configurations/SensorMeasurementConfiguration.cs b/samples/Eftdb.Samples.Shared/Configurations/SensorMeasurementConfiguration.cs
index 7a86fbb..e4dd13b 100644
--- a/samples/Eftdb.Samples.Shared/Configurations/SensorMeasurementConfiguration.cs
+++ b/samples/Eftdb.Samples.Shared/Configurations/SensorMeasurementConfiguration.cs
@@ -14,9 +14,10 @@ public void Configure(EntityTypeBuilder builder)
builder.IsHypertable(x => x.RecordedAt)
.WithChunkTimeInterval("1 day")
- .EnableCompression()
- .WithCompressionSegmentBy(x => x.SensorId)
- .WithCompressionOrderBy(s => [s.ByDescending(x => x.RecordedAt)]);
+ .WithCompressionSegmentBy(x => x.Site, x => x.MetricType)
+ .WithCompressionOrderBy(
+ s => s.By(x => x.SensorId),
+ s => s.ByDescending(x => x.RecordedAt));
}
}
}
diff --git a/samples/Eftdb.Samples.Shared/Models/DeviceReading.cs b/samples/Eftdb.Samples.Shared/Models/DeviceReading.cs
index 69aebc4..cbe8c42 100644
--- a/samples/Eftdb.Samples.Shared/Models/DeviceReading.cs
+++ b/samples/Eftdb.Samples.Shared/Models/DeviceReading.cs
@@ -4,7 +4,7 @@
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models
{
- [Hypertable(nameof(Time), ChunkSkipColumns = new[] { "Time" }, ChunkTimeInterval = "1 day", EnableCompression = true, CompressionSegmentBy = new[] { "DeviceId" }, CompressionOrderBy = new[] { "Time DESC" })]
+ [Hypertable(nameof(Time), ChunkSkipColumns = new[] { "Time" }, ChunkTimeInterval = "1 day", EnableCompression = true, CompressionSegmentBy = new[] { "DeviceId" }, CompressionOrderBy = new[] { $"{nameof(Time)} DESC" })]
[Index(nameof(Time), Name = "ix_device_readings_time")]
[PrimaryKey(nameof(Id), nameof(Time))]
[ReorderPolicy("ix_device_readings_time", InitialStart = "2025-09-23T09:15:19.3905112Z", ScheduleInterval = "1 day", MaxRuntime = "00:00:00", RetryPeriod = "00:05:00", MaxRetries = 3)]
diff --git a/samples/Eftdb.Samples.Shared/Models/SensorMeasurement.cs b/samples/Eftdb.Samples.Shared/Models/SensorMeasurement.cs
index afc829e..8e8796e 100644
--- a/samples/Eftdb.Samples.Shared/Models/SensorMeasurement.cs
+++ b/samples/Eftdb.Samples.Shared/Models/SensorMeasurement.cs
@@ -3,14 +3,23 @@
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models
{
///
- /// IoT sensor measurement whose time column is a NodaTime .
+ /// IoT sensor measurement in long format, whose time column is a NodaTime .
///
public class SensorMeasurement
{
public Guid Id { get; set; }
public Instant RecordedAt { get; set; }
+
+ /// Physical site the reading originates from (e.g. "berlin-dc1"). Low cardinality.
+ public string Site { get; set; } = string.Empty;
+
+ /// Kind of metric recorded (e.g. "temperature", "humidity"). Low cardinality.
+ public string MetricType { get; set; } = string.Empty;
+
+ /// Identifier of the individual sensor reporting the value. Higher cardinality.
public string SensorId { get; set; } = string.Empty;
- public double Temperature { get; set; }
- public double Humidity { get; set; }
+
+ /// The measured value for the given metric type.
+ public double Value { get; set; }
}
}
diff --git a/src/Eftdb.Design/Eftdb.Design.csproj b/src/Eftdb.Design/Eftdb.Design.csproj
index b7aae2c..8134e29 100644
--- a/src/Eftdb.Design/Eftdb.Design.csproj
+++ b/src/Eftdb.Design/Eftdb.Design.csproj
@@ -45,4 +45,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/Eftdb.Design/Generators/AnnotationRenderers/AnnotationRendererHelper.cs b/src/Eftdb.Design/Generators/AnnotationRenderers/AnnotationRendererHelper.cs
new file mode 100644
index 0000000..289658f
--- /dev/null
+++ b/src/Eftdb.Design/Generators/AnnotationRenderers/AnnotationRendererHelper.cs
@@ -0,0 +1,82 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers
+{
+ ///
+ /// Hhelpers shared by implementations:
+ /// annotation lookup and consumption, column list parsing, and database-column to CLR-property
+ /// resolution.
+ ///
+ internal static class AnnotationRendererHelper
+ {
+ public static PropertyAccessorCodeFragment PropertyAccessor(string property) => new("x", [property]);
+
+ ///
+ /// Maps a scaffolded database column name to its CLR property name on the entity, so generated code
+ /// references the property (e.g. DeviceId) rather than the raw column (e.g. device_id).
+ ///
+ public static string ResolvePropertyName(IEntityType entityType, string columnName)
+ {
+ TryResolvePropertyName(entityType, columnName, out string propertyName);
+ return propertyName;
+ }
+
+ ///
+ /// Maps a scaffolded database column name to its CLR property name. Returns false with the
+ /// raw value when no column mapping or property resolves, so callers can avoid emitting
+ /// nameof(...) references to members that do not exist.
+ ///
+ public static bool TryResolvePropertyName(IEntityType entityType, string columnName, out string propertyName)
+ {
+ StoreObjectIdentifier? store =
+ StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)
+ ?? StoreObjectIdentifier.Create(entityType, StoreObjectType.View);
+
+ if (store is StoreObjectIdentifier id)
+ {
+ foreach (IProperty property in entityType.GetProperties())
+ {
+ if (string.Equals(property.GetColumnName(id), columnName, StringComparison.Ordinal))
+ {
+ propertyName = property.Name;
+ return true;
+ }
+ }
+ }
+
+ if (entityType.FindProperty(columnName) is IProperty direct)
+ {
+ propertyName = direct.Name;
+ return true;
+ }
+
+ propertyName = columnName;
+ return false;
+ }
+
+ public static string[] ResolveColumns(IEntityType entityType, string? value)
+ => [.. SplitColumns(value).Select(column => ResolvePropertyName(entityType, column))];
+
+ public static IAnnotation? Find(IDictionary annotations, string key)
+ => annotations.TryGetValue(key, out IAnnotation? annotation) ? annotation : null;
+
+ public static string? GetString(IDictionary annotations, string key)
+ => Find(annotations, key)?.Value as string;
+
+ public static string[] SplitColumns(string? value)
+ => string.IsNullOrWhiteSpace(value)
+ ? []
+ : [.. value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+
+ public static void Consume(IDictionary annotations, params string[] keys)
+ {
+ foreach (string key in keys)
+ {
+ annotations.Remove(key);
+ }
+ }
+ }
+}
diff --git a/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregateAnnotationRenderer.cs b/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregateAnnotationRenderer.cs
new file mode 100644
index 0000000..cdbad1d
--- /dev/null
+++ b/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregateAnnotationRenderer.cs
@@ -0,0 +1,294 @@
+using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregate;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Design.Scaffolding;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Internals;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using System.Reflection;
+using static CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers.AnnotationRendererHelper;
+
+#pragma warning disable EF1001 // Suppress warning about internal APIs usage, common for providers/extensions
+namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers
+{
+ ///
+ /// Renders ContinuousAggregate annotations as IsContinuousAggregate(...) Fluent API chains or
+ /// [ContinuousAggregate] attributes. The ViewDefinition annotation is parsed to reconstruct
+ /// the structured configuration; on success it is consumed so no raw SQL leaks into the generated file.
+ /// When the view definition cannot be parsed, a warning is reported and the annotations are left in
+ /// place so the .HasAnnotation(...) fallback preserves the configuration.
+ ///
+ internal sealed class ContinuousAggregateAnnotationRenderer(IOperationReporter reporter) : IFeatureAnnotationRenderer
+ {
+ private static readonly MethodInfo IsContinuousAggregateMethod =
+ typeof(ContinuousAggregateTypeBuilder)
+ .GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .First(m => m.Name == nameof(ContinuousAggregateTypeBuilder.IsContinuousAggregate)
+ && m.GetParameters().Length == 5);
+
+ private static MethodInfo BuilderMethod(string name) =>
+ typeof(ContinuousAggregateStringBuilder<>)
+ .GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .First(m => m.Name == name);
+
+ private static readonly MethodInfo AddAggregateFunctionMethod = BuilderMethod("AddAggregateFunction");
+ private static readonly MethodInfo AddGroupByColumnMethod = BuilderMethod("AddGroupByColumn");
+ private static readonly MethodInfo WhereMethod = BuilderMethod("Where");
+ private static readonly MethodInfo MaterializedOnlyMethod = BuilderMethod("MaterializedOnly");
+ private static readonly MethodInfo WithNoDataMethod = BuilderMethod("WithNoData");
+ private static readonly MethodInfo CreateGroupIndexesMethod = BuilderMethod("CreateGroupIndexes");
+ private static readonly MethodInfo WithChunkIntervalMethod = BuilderMethod("WithChunkInterval");
+
+ public IReadOnlyList GenerateFluentApiCalls(
+ IEntityType entityType, IDictionary annotations)
+ {
+ string? materializedViewName = GetString(annotations, ContinuousAggregateAnnotations.MaterializedViewName);
+ if (materializedViewName is null)
+ {
+ return [];
+ }
+
+ string? viewDefinition = GetString(annotations, ContinuousAggregateAnnotations.ViewDefinition);
+ string? parentName = GetString(annotations, ContinuousAggregateAnnotations.ParentName);
+ string? chunkInterval = GetString(annotations, ContinuousAggregateAnnotations.ChunkInterval);
+ bool materializedOnly = Find(annotations, ContinuousAggregateAnnotations.MaterializedOnly)?.Value is true;
+ bool withNoData = Find(annotations, ContinuousAggregateAnnotations.WithNoData)?.Value is true;
+ bool createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool? ?? true;
+
+ ViewDefinitionParser.ParsedViewDefinition? parsed = viewDefinition is not null
+ ? ViewDefinitionParser.Parse(viewDefinition)
+ : null;
+
+ if (parsed?.TimeBucketWidth is null || parsed.TimeBucketSourceColumn is null)
+ {
+ ReportUnparseableViewDefinition(materializedViewName);
+ return [];
+ }
+
+ string humanizedWidth = IntervalParsingHelper.NormalizeInterval(parsed.TimeBucketWidth);
+
+ IEntityType? parentEntityType = ParentEntityTypeResolver.Resolve(entityType.Model, parentName);
+ string parentClrName = parentEntityType?.ShortName() ?? parentName ?? materializedViewName;
+
+ object parentNameArg = parentEntityType is not null
+ ? new NameOfCodeFragment(parentClrName)
+ : (object)(parentName ?? string.Empty);
+
+ object timeBucketArg = ResolveParentColumnArg(parentEntityType, parentClrName, parsed.TimeBucketSourceColumn);
+
+ MethodCallCodeFragment call = new(IsContinuousAggregateMethod, materializedViewName, parentNameArg, humanizedWidth, timeBucketArg);
+
+ if (materializedOnly)
+ {
+ call = call.Chain(MaterializedOnlyMethod, true);
+ }
+
+ if (withNoData)
+ {
+ call = call.Chain(WithNoDataMethod, true);
+ }
+
+ if (!createGroupIndexes)
+ {
+ call = call.Chain(CreateGroupIndexesMethod, false);
+ }
+
+ string caEntityClrName = entityType.ShortName();
+
+ foreach (ViewDefinitionParser.ParsedAggregate agg in parsed.Aggregates)
+ {
+ object aliasArg = TryResolvePropertyName(entityType, agg.Alias, out string aliasProperty)
+ ? new NameOfCodeFragment($"{caEntityClrName}.{aliasProperty}")
+ : (object)agg.Alias;
+
+ object sourceArg = agg.SourceColumn == "*"
+ ? (object)"*"
+ : ResolveParentColumnArg(parentEntityType, parentClrName, agg.SourceColumn);
+
+ call = call.Chain(AddAggregateFunctionMethod, [aliasArg, sourceArg, agg.Function]);
+ }
+
+ foreach (string col in parsed.GroupByColumns)
+ {
+ call = call.Chain(AddGroupByColumnMethod, ResolveParentColumnArg(parentEntityType, parentClrName, col));
+ }
+
+ if (!string.IsNullOrWhiteSpace(parsed.WhereClause))
+ {
+ call = call.Chain(WhereMethod, parsed.WhereClause);
+ }
+
+ if (!string.IsNullOrWhiteSpace(chunkInterval) && !IsDerivedDefaultChunkInterval(chunkInterval, parentEntityType))
+ {
+ call = call.Chain(WithChunkIntervalMethod, IntervalParsingHelper.NormalizeInterval(chunkInterval));
+ }
+
+ ConsumeAllCaAnnotations(annotations);
+ return [call];
+ }
+
+ public IReadOnlyList GenerateDataAnnotationAttributes(
+ IEntityType entityType, IDictionary annotations)
+ {
+ string? materializedViewName = GetString(annotations, ContinuousAggregateAnnotations.MaterializedViewName);
+ if (materializedViewName is null)
+ {
+ return [];
+ }
+
+ string? viewDefinition = GetString(annotations, ContinuousAggregateAnnotations.ViewDefinition);
+ string? parentName = GetString(annotations, ContinuousAggregateAnnotations.ParentName);
+ string? chunkInterval = GetString(annotations, ContinuousAggregateAnnotations.ChunkInterval);
+ bool materializedOnly = Find(annotations, ContinuousAggregateAnnotations.MaterializedOnly)?.Value is true;
+ bool withNoData = Find(annotations, ContinuousAggregateAnnotations.WithNoData)?.Value is true;
+ bool createGroupIndexes = Find(annotations, ContinuousAggregateAnnotations.CreateGroupIndexes)?.Value as bool? ?? true;
+
+ ViewDefinitionParser.ParsedViewDefinition? parsed = viewDefinition is not null
+ ? ViewDefinitionParser.Parse(viewDefinition)
+ : null;
+
+ if (parsed?.TimeBucketWidth is null || parsed.TimeBucketSourceColumn is null)
+ {
+ return [];
+ }
+
+ ConsumeAllCaAnnotations(annotations);
+
+ string humanizedWidth = IntervalParsingHelper.NormalizeInterval(parsed.TimeBucketWidth);
+
+ IEntityType? parentEntityType = ParentEntityTypeResolver.Resolve(entityType.Model, parentName);
+ string parentClrName = parentEntityType?.ShortName() ?? parentName ?? materializedViewName;
+
+ object parentNameArg = parentEntityType is not null
+ ? new NameOfCodeFragment(parentClrName)
+ : (object)(parentName ?? string.Empty);
+
+ object timeBucketArg = ResolveParentColumnArg(parentEntityType, parentClrName, parsed.TimeBucketSourceColumn);
+
+ ReportUnrepresentableGroupByEntries(entityType, materializedViewName, parsed.GroupByColumns);
+
+ Dictionary caNamedArgs = new()
+ {
+ [nameof(ContinuousAggregateAttribute.MaterializedViewName)] = materializedViewName,
+ [nameof(ContinuousAggregateAttribute.ParentName)] = parentNameArg,
+ };
+
+ if (!string.IsNullOrWhiteSpace(chunkInterval) && !IsDerivedDefaultChunkInterval(chunkInterval, parentEntityType))
+ {
+ caNamedArgs[nameof(ContinuousAggregateAttribute.ChunkInterval)] = IntervalParsingHelper.NormalizeInterval(chunkInterval);
+ }
+
+ if (materializedOnly)
+ {
+ caNamedArgs[nameof(ContinuousAggregateAttribute.MaterializedOnly)] = true;
+ }
+
+ if (withNoData)
+ {
+ caNamedArgs[nameof(ContinuousAggregateAttribute.WithNoData)] = true;
+ }
+
+ if (!createGroupIndexes)
+ {
+ caNamedArgs[nameof(ContinuousAggregateAttribute.CreateGroupIndexes)] = false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(parsed.WhereClause))
+ {
+ caNamedArgs[nameof(ContinuousAggregateAttribute.Where)] = parsed.WhereClause;
+ }
+
+ return [
+ new AttributeCodeFragment(typeof(ContinuousAggregateAttribute), [], caNamedArgs),
+ new AttributeCodeFragment(typeof(TimeBucketAttribute), humanizedWidth, timeBucketArg),
+ ];
+ }
+
+ public void ConsumeFeatureAnnotations(IEntityType entityType, IDictionary annotations)
+ {
+ string? materializedViewName = GetString(annotations, ContinuousAggregateAnnotations.MaterializedViewName);
+ if (materializedViewName is null)
+ {
+ return;
+ }
+
+ string? viewDefinition = GetString(annotations, ContinuousAggregateAnnotations.ViewDefinition);
+ ViewDefinitionParser.ParsedViewDefinition? parsed = viewDefinition is not null
+ ? ViewDefinitionParser.Parse(viewDefinition)
+ : null;
+
+ if (parsed?.TimeBucketWidth is null || parsed.TimeBucketSourceColumn is null)
+ {
+ ReportUnparseableViewDefinition(materializedViewName);
+ return;
+ }
+
+ ConsumeAllCaAnnotations(annotations);
+ }
+
+ ///
+ /// A continuous aggregate's chunk interval defaults to 10x the parent hypertable's chunk
+ /// interval; a value equal to that derived default is elided, symmetric with the hypertable's
+ /// 7-days elision. Calendar-unit intervals fail the microsecond conversion and are kept.
+ ///
+ private static bool IsDerivedDefaultChunkInterval(string chunkInterval, IEntityType? parentEntityType)
+ {
+ string parentChunkInterval = parentEntityType?.FindAnnotation(HypertableAnnotations.ChunkTimeInterval)?.Value as string
+ ?? DefaultValues.ChunkTimeInterval;
+
+ return IntervalParsingHelper.TryGetTotalMicroseconds(chunkInterval, out long caMicroseconds)
+ && IntervalParsingHelper.TryGetTotalMicroseconds(parentChunkInterval, out long parentMicroseconds)
+ && caMicroseconds == 10 * parentMicroseconds;
+ }
+
+ private void ReportUnparseableViewDefinition(string materializedViewName)
+ => reporter.WriteWarning(
+ $"The view definition of continuous aggregate '{materializedViewName}' could not be parsed. " +
+ "Its configuration is preserved as .HasAnnotation(...) calls; migrations will recreate the " +
+ "view from the raw SQL definition.");
+
+ ///
+ /// GROUP BY entries that match no property on the aggregate entity (raw SQL expressions or
+ /// unmapped columns) have no data-annotation representation and would be silently lost.
+ ///
+ private void ReportUnrepresentableGroupByEntries(
+ IEntityType entityType, string materializedViewName, IReadOnlyList groupByColumns)
+ {
+ foreach (string col in groupByColumns)
+ {
+ if (!TryResolvePropertyName(entityType, col, out _))
+ {
+ reporter.WriteWarning(
+ $"The GROUP BY expression '{col}' of continuous aggregate '{materializedViewName}' " +
+ "cannot be represented as a data annotation. Scaffold without --data-annotations or " +
+ "configure it manually via AddGroupByColumn(...).");
+ }
+ }
+ }
+
+ private static object ResolveParentColumnArg(IEntityType? parentEntityType, string parentClrName, string columnName)
+ => parentEntityType is not null && TryResolvePropertyName(parentEntityType, columnName, out string propName)
+ ? new NameOfCodeFragment($"{parentClrName}.{propName}")
+ : (object)columnName;
+
+ private static void ConsumeAllCaAnnotations(IDictionary annotations)
+ {
+ Consume(annotations,
+ ContinuousAggregateAnnotations.MaterializedViewName,
+ ContinuousAggregateAnnotations.ParentName,
+ ContinuousAggregateAnnotations.MaterializedOnly,
+ ContinuousAggregateAnnotations.ChunkInterval,
+ ContinuousAggregateAnnotations.ViewDefinition,
+ ContinuousAggregateAnnotations.TimeBucketWidth,
+ ContinuousAggregateAnnotations.TimeBucketSourceColumn,
+ ContinuousAggregateAnnotations.TimeBucketGroupBy,
+ ContinuousAggregateAnnotations.AggregateFunctions,
+ ContinuousAggregateAnnotations.GroupByColumns,
+ ContinuousAggregateAnnotations.WhereClause,
+ ContinuousAggregateAnnotations.WithNoData,
+ ContinuousAggregateAnnotations.CreateGroupIndexes);
+ }
+ }
+}
+#pragma warning restore EF1001
diff --git a/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregatePolicyAnnotationRenderer.cs b/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregatePolicyAnnotationRenderer.cs
new file mode 100644
index 0000000..5c70267
--- /dev/null
+++ b/src/Eftdb.Design/Generators/AnnotationRenderers/ContinuousAggregatePolicyAnnotationRenderer.cs
@@ -0,0 +1,200 @@
+using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregate;
+using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregatePolicy;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using System.Reflection;
+using static CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers.AnnotationRendererHelper;
+using static CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers.PolicyJobRendererHelper;
+
+namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators.AnnotationRenderers
+{
+ ///
+ /// Renders TimescaleDB:ContinuousAggregatePolicy:* annotations as a
+ /// .WithRefreshPolicy(...) Fluent API chain or a [ContinuousAggregatePolicy(...)]
+ /// attribute.
+ ///
+ internal sealed class ContinuousAggregatePolicyAnnotationRenderer : IFeatureAnnotationRenderer
+ {
+ private static readonly MethodInfo WithRefreshPolicyMethod =
+ typeof(ContinuousAggregateBuilderPolicyExtensions)
+ .GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .First(m => m.Name == nameof(ContinuousAggregateBuilderPolicyExtensions.WithRefreshPolicy)
+ && m.GetParameters().Length == 4
+ && m.GetParameters()[0].ParameterType.IsGenericType
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ContinuousAggregateStringBuilder<>));
+
+ private static MethodInfo PolicyBuilderMethod(string name) =>
+ typeof(ContinuousAggregatePolicyStringBuilder<>)
+ .GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .First(m => m.Name == name);
+
+ private static readonly MethodInfo WithInitialStartMethod = PolicyBuilderMethod(nameof(ContinuousAggregatePolicyStringBuilder