Skip to content

Commit 0ad1fe7

Browse files
Merge pull request #51 from cmdscale/feature/rework_migration_code_generation
Rework migration code generation
2 parents f1b08bc + 97327b0 commit 0ad1fe7

77 files changed

Lines changed: 6484 additions & 1378 deletions

File tree

Some content is hidden

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

‎.claude/CLAUDE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ private class TestContext(string connectionString) : DbContext { }
8686
| Service Registration | `UseTimescaleDb()` configures all services | `reference/patterns.md` |
8787
| Convention System | `IEntityTypeAddedConvention` processes attributes | `reference/patterns.md` |
8888
| Dual Configuration | Annotations + Fluent API → same annotations | `reference/patterns.md` |
89-
| IFeatureDiffer | Per-feature differ with model extractor | `reference/patterns.md` |
90-
| Runtime vs Design-Time | `isDesignTime` parameter changes quote escaping | `reference/patterns.md` |
89+
| IFeatureDiffer | Per-feature differ with model extractor + `FeatureDiffContext` | `reference/patterns.md` |
90+
| Runtime vs Design-Time | `*SqlGenerator` (SQL) vs `*CSharpGenerator` (typed migration calls) | `reference/patterns.md` |
9191
| Column Name Resolution | Always use `StoreObjectIdentifier` + `GetColumnName()` | `reference/patterns.md` |
9292

9393
## Agent Workflow

‎.claude/agents/eftdb-bug-fixer.md‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: eftdb-bug-fixer
3-
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<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\nContext: SQL generation is incorrect for reorder policies.\nuser: "The ReorderPolicyOperationGenerator is generating invalid SQL with wrong schema qualification"\nassistant: "I'll launch the eftdb-bug-fixer agent to fix the SQL generation bug in ReorderPolicyOperationGenerator."\n<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>
3+
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<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>\n\n<example>\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<uses Task tool to invoke eftdb-bug-fixer>\n</example>
44
model: sonnet
55
color: red
66
---
@@ -38,7 +38,9 @@ You are an elite debugging and code quality specialist for the CmdScale.EntityFr
3838
- Identify which component is affected:
3939
- Model Extractor (reads annotations from EF model)
4040
- Differ (compares models and generates operations)
41-
- Operation Generator (generates SQL/C# code)
41+
- SQL Generator (`Generators/[Feature]SqlGenerator.cs` — runtime SQL)
42+
- C# Generator (`Design/Generators/[Feature]CSharpGenerator.cs` — typed migration calls)
43+
- Migration Extensions (`MigrationExtensions/[Feature]MigrationExtensions.cs`)
4244
- Scaffolding Extractor (queries TimescaleDB catalog)
4345
- Scaffolding Applier (applies annotations to scaffolded model)
4446
- Convention (converts attributes to annotations)
@@ -72,10 +74,10 @@ Before fixing, understand WHY the bug exists:
7274
- Hard-coded column names instead of convention-aware resolution
7375

7476
3. **SQL Generation Bugs:**
75-
- Quote string not respected (`isDesignTime` parameter ignored)
77+
- Identifiers not quoted via `SqlBuilderHelper` (`Regclass`/`QualifiedIdentifier`/`QuoteIdentifier`)
7678
- Schema qualification missing or incorrect
7779
- SQL syntax errors for specific TimescaleDB functions
78-
- Parameter escaping issues
80+
- Missing `suppressTransaction` for DDL that cannot run in a transaction (continuous aggregates)
7981

8082
4. **Null Reference Issues:**
8183
- Missing null checks for optional properties
@@ -88,9 +90,9 @@ Before fixing, understand WHY the bug exists:
8890
- Type conversion issues (string vs long for intervals)
8991

9092
6. **Design-Time vs Runtime Confusion:**
91-
- Generator not handling `isDesignTime` parameter correctly
92-
- Quote escaping wrong for C# string generation
93-
- Operation registered in runtime but not in design-time generator
93+
- Operation registered in the runtime `TimescaleDbMigrationsSqlGenerator` switch but not in the design-time `TimescaleCSharpMigrationOperationGenerator` switch (or vice versa)
94+
- Missing `MigrationExtensions` method so generated migrations cannot call the operation
95+
- Runtime SQL and design-time typed call producing inconsistent results
9496

9597
### Phase 3: Fix Implementation
9698

@@ -160,12 +162,12 @@ string columnName = property.GetColumnName(storeIdentifier);
160162
```
161163

162164
```csharp
163-
// Bug: Quote string not used in SQL generation
165+
// Bug: identifier not quoted via the helper
164166
// INCORRECT FIX - Hard-coded quotes
165167
string sql = $"SELECT * FROM \"{schema}\".\"{table}\"";
166168

167-
// CORRECT FIX - Use quote string field and SqlBuilderHelper
168-
string qualifiedName = SqlBuilderHelper.GetQualifiedTableName(schema, table, _quoteString);
169+
// CORRECT FIX - Use SqlBuilderHelper
170+
string qualifiedName = SqlBuilderHelper.QualifiedIdentifier(table, schema);
169171
string sql = $"SELECT * FROM {qualifiedName}";
170172
```
171173

@@ -212,14 +214,11 @@ if (annotation.Value is not string expectedValue)
212214

213215
### For SQL Generation Issues:
214216
```csharp
215-
// Verify quote string usage
216-
System.Diagnostics.Debug.WriteLine($"Quote string: '{_quoteString}'");
217-
System.Diagnostics.Debug.WriteLine($"Generated SQL: {sql}");
218-
219-
// Check if isDesignTime is propagated correctly
220-
if (isDesignTime && !sql.Contains("\"\""))
217+
// Inspect the statements returned by the feature SqlGenerator
218+
List<string> statements = HypertableSqlGenerator.Generate(operation);
219+
foreach (string statement in statements)
221220
{
222-
// Likely bug: design-time should have doubled quotes
221+
System.Diagnostics.Debug.WriteLine($"Generated SQL: {statement}");
223222
}
224223
```
225224

‎.claude/agents/eftdb-feature-implementer.md‎

Lines changed: 43 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ You are an elite Entity Framework Core migrations architect specializing in the
1111

1212
**PROJECT SCOPE RESTRICTION**: You MUST NOT modify code in any project except:
1313
- CmdScale.EntityFrameworkCore.TimescaleDB (primary work area)
14-
- CmdScale.EntityFrameworkCore.TimescaleDB.Design (ONLY the TimescaleCSharpMigrationOperationGenerator.cs file)
14+
- CmdScale.EntityFrameworkCore.TimescaleDB.Design (the `Generators/[Feature]CSharpGenerator.cs` file and `TimescaleCSharpMigrationOperationGenerator.cs`)
1515

1616
Any attempt to modify other projects should result in immediate rejection with explanation.
1717

@@ -48,105 +48,58 @@ Implement the following components in this exact order:
4848

4949
#### 2. Feature Differ (Internals/Features/[Feature]Differ.cs)
5050

51-
- Implement `IFeatureDiffer` interface
52-
- Use the extractor to compare source and target models
53-
- Generate appropriate operations (Create, Alter, Drop) based on differences
54-
- Return `IEnumerable<MigrationOperation>` with proper priority values:
55-
- Priority 0: Standard EF operations
56-
- Priority 10: CreateHypertableOperation
57-
- Priority 20: Reorder policies
58-
- Priority 30: Create continuous aggregates
59-
- Priority 40: Alter/Drop continuous aggregates
60-
- Choose appropriate priority for your feature based on dependencies
61-
- Follow existing patterns from HypertableDiffer, ReorderPolicyDiffer, or ContinuousAggregateDiffer
51+
- Implement `IFeatureDiffer`: `IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source, IRelationalModel? target, FeatureDiffContext? context = null)`
52+
- Normalize `context ??= FeatureDiffContext.Empty;` and use it to resolve renames (`ResolveTable`, `ResolveColumn`, `ResolveIndex`) so a rename is not treated as drop-and-create
53+
- Use the extractor to compare source and target models, generating Create/Alter/Drop operations
54+
- Operation ordering is handled centrally by `GetOperationPriority()` (see step 3) — the differ does not set priorities itself
55+
- Follow existing patterns from HypertableDiffer, ReorderPolicyDiffer, RetentionPolicyDiffer, or ContinuousAggregateDiffer
6256

6357
#### 3. Update TimescaleMigrationsModelDiffer (Internals/TimescaleMigrationsModelDiffer.cs)
6458

65-
- Register your new differ in the constructor's `_featureDiffers` list
66-
- Ensure it's positioned correctly based on dependency order
67-
- No other changes needed to this file
59+
- Invoke your new differ in `GetDifferences()`, passing the shared `FeatureDiffContext`
60+
- Add a `case` for each new operation type in `GetOperationPriority()` (drops negative, adds/alters positive; pick values matching the feature's dependency order — see the priority table in `reference/architecture.md`)
6861

69-
#### 4. Operation Generator (Generators/[Feature]OperationGenerator.cs)
62+
#### 4. Runtime SQL Generator (Generators/[Feature]SqlGenerator.cs)
7063

71-
- Create a class that handles both SQL generation and C# code generation
72-
- **CRITICAL**: Constructor MUST have `isDesignTime` parameter with default value `false`:
73-
```csharp
74-
public FeatureOperationGenerator(bool isDesignTime = false)
75-
{
76-
_quoteString = isDesignTime ? "\"\"" : "\"";
77-
}
78-
```
79-
- Use `_quoteString` for all string literals in SQL generation
80-
- Implement these methods for each operation type:
81-
- `Generate([Operation] operation, IModel? model, MigrationCommandListBuilder builder, bool isDesignTime)` - Runtime SQL
82-
- `Generate([Operation] operation, CSharpMigrationOperationBuilder builder)` - Design-time C# code
83-
- Use `SqlBuilderHelper` static methods for table names, schema handling, and identifier quoting:
84-
- `GetQualifiedTableName(schema, table, quoteString)` for fully qualified names
85-
- `GetSchemaPrefix(schema, quoteString)` for schema prefixing
86-
- Always pass your `_quoteString` to these methods
87-
- Follow SQL generation patterns from existing generators (HypertableOperationGenerator, ReorderPolicyOperationGenerator)
88-
89-
#### 5. Update TimescaleDbMigrationsSqlGenerator (TimescaleDbMigrationsSqlGenerator.cs)
90-
91-
- Add method to handle your operation type:
92-
```csharp
93-
protected virtual void Generate([YourOperation] operation, IModel? model, MigrationCommandListBuilder builder)
94-
{
95-
var generator = new YourOperationGenerator(_isDesignTime);
96-
generator.Generate(operation, model, builder, _isDesignTime);
97-
}
98-
```
99-
- Store `isDesignTime` parameter in a field: `private readonly bool _isDesignTime;`
100-
- Pass it through to generators
64+
- Static class exposing `static List<string> Generate(XxxOperation operation)` per operation type, returning TimescaleDB SQL statements
65+
- Build identifiers with `SqlBuilderHelper.Regclass()`, `SqlBuilderHelper.QualifiedIdentifier()`, `SqlBuilderHelper.QuoteIdentifier()`
66+
- For policy scheduling SQL (`alter_job` clauses), reuse `PolicyJobSqlBuilder`
67+
- Follow existing generators (HypertableSqlGenerator, RetentionPolicySqlGenerator)
10168

102-
#### 6. Update TimescaleCSharpMigrationOperationGenerator (Design Project - ONLY FILE ALLOWED)
69+
#### 5. Typed Migration Extensions (MigrationExtensions/[Feature]MigrationExtensions.cs)
10370

104-
- Add C# code generation method:
105-
```csharp
106-
protected virtual void Generate([YourOperation] operation, CSharpMigrationOperationBuilder builder)
107-
{
108-
var generator = new YourOperationGenerator(isDesignTime: true);
109-
generator.Generate(operation, builder);
110-
}
111-
```
112-
- This is the ONLY file in the Design project you may modify
71+
- Add extension methods on `MigrationBuilder` (declared in namespace `Microsoft.EntityFrameworkCore.Migrations`) that construct the operation and `migrationBuilder.Operations.Add(operation)`
72+
- Return an `OperationBuilder<XxxOperation>`
73+
- These are the methods generated migrations call (e.g. `migrationBuilder.CreateHypertable(...)`)
74+
75+
#### 6. Register in TimescaleDbMigrationsSqlGenerator (TimescaleDbMigrationsSqlGenerator.cs)
76+
77+
- Add a `case XxxOperation op:` to the `Generate` switch that calls `[Feature]SqlGenerator.Generate(op)` and assigns `statements`
78+
- Set `suppressTransaction = true` for operations whose DDL cannot run in a transaction (e.g. continuous-aggregate creation)
79+
80+
#### 7. Design-Time C# Generator (Design/Generators/[Feature]CSharpGenerator.cs + register)
81+
82+
- `Generate(XxxOperation operation, IndentedStringBuilder builder)` emits the typed `migrationBuilder.[Method](...)` call using `MigrationCallWriter` and `CSharpGeneratorHelper`
83+
- Emit a named `call.Arg("argName", code.Literal(...))` for each value, skipping defaults/empties
84+
- Register the operation type in the `switch` in `TimescaleCSharpMigrationOperationGenerator.cs`
11385

11486
## Critical Technical Requirements
11587

116-
### Quote String Handling
88+
### Runtime vs Design-Time Split
11789

118-
**This is ABSOLUTELY CRITICAL for runtime vs design-time duality:**
90+
The two paths are independent and consume the same operation types:
11991

120-
- **Runtime Migrations** (`dotnet ef database update`):
121-
- Quote string: `"` (single quote)
122-
- Generates raw SQL that executes against database
123-
124-
- **Design-Time Migrations** (`dotnet ef migrations add`):
125-
- Quote string: `""` (doubled quotes)
126-
- Generates C# code with escaped strings for migration files
92+
- **Runtime** (`dotnet ef database update`): `TimescaleDbMigrationsSqlGenerator` → `[Feature]SqlGenerator.Generate(operation)` → SQL statements.
93+
- **Design-time** (`dotnet ef migrations add`): `TimescaleCSharpMigrationOperationGenerator` → `[Feature]CSharpGenerator.Generate(operation, builder)` → typed `migrationBuilder.[Method](...)` calls.
12794

128-
**Implementation Pattern:**
129-
```csharp
130-
public class YourOperationGenerator
131-
{
132-
private readonly string _quoteString;
133-
134-
public YourOperationGenerator(bool isDesignTime = false)
135-
{
136-
_quoteString = isDesignTime ? "\"\"" : "\"";
137-
}
138-
139-
// Use _quoteString in all SQL generation
140-
var tableName = SqlBuilderHelper.GetQualifiedTableName(schema, table, _quoteString);
141-
}
142-
```
95+
Generators carry no `isDesignTime` flag and do no quote-doubling.
14396

14497
### SqlBuilderHelper Usage
14598

146-
ALWAYS use SqlBuilderHelper for:
147-
- Table name qualification: `GetQualifiedTableName(schema, table, quoteString)`
148-
- Schema prefixing: `GetSchemaPrefix(schema, quoteString)`
149-
- Identifier quoting: Methods handle this internally when you pass quoteString
99+
In `[Feature]SqlGenerator`, build identifiers with:
100+
- `SqlBuilderHelper.Regclass(table, schema)` → `'schema."table"'` (for `create_hypertable` and other regclass arguments)
101+
- `SqlBuilderHelper.QualifiedIdentifier(table, schema)` → `"schema"."table"` (for `ALTER TABLE` etc.)
102+
- `SqlBuilderHelper.QuoteIdentifier(column)` → `"column"`
150103

151104
NEVER manually construct qualified names or handle quoting yourself.
152105

@@ -243,10 +196,12 @@ Once the feature initializer completes, relaunch this agent to implement the mig
243196
Implemented Components:
244197
- Internals/Features/[Feature]/[Feature]ModelExtractor.cs
245198
- Internals/Features/[Feature]/[Feature]Differ.cs
246-
- Generators/[Feature]OperationGenerator.cs
247-
- Updated: Internals/TimescaleMigrationsModelDiffer.cs
248-
- Updated: TimescaleDbMigrationsSqlGenerator.cs
249-
- Updated: Design/TimescaleCSharpMigrationOperationGenerator.cs
199+
- Generators/[Feature]SqlGenerator.cs
200+
- MigrationExtensions/[Feature]MigrationExtensions.cs
201+
- Design/Generators/[Feature]CSharpGenerator.cs
202+
- Updated: Internals/TimescaleMigrationsModelDiffer.cs (differ invocation + GetOperationPriority cases)
203+
- Updated: TimescaleDbMigrationsSqlGenerator.cs (Generate switch case)
204+
- Updated: Design/TimescaleCSharpMigrationOperationGenerator.cs (Generate switch case)
250205
251206
Operation Priority: [X] (rationale: [explanation])
252207

‎.claude/agents/eftdb-feature-initializer.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Created Files:
141141
142142
NEXT STEPS:
143143
→ Use eftdb-feature-implementer agent to implement migration logic
144-
(Creates: Differ, ModelExtractor, OperationGenerator)
144+
(Creates: Differ, ModelExtractor, SqlGenerator, MigrationExtensions, CSharpGenerator)
145145
146146
→ Then use eftdb-scaffold-support agent for db-first scaffolding
147147
(Creates: ScaffoldingExtractor, AnnotationApplier)

‎.claude/agents/eftdb-scaffold-support.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ You are ONLY permitted to work within:
1616
You are ABSOLUTELY FORBIDDEN from:
1717
- Modifying any files in other projects (Runtime, Tests, Example, etc.)
1818
- Fixing bugs you discover in other projects
19-
- Changing operation generators, differs, or migration code
19+
- Changing SQL/C# generators, migration extensions, differs, or migration code
2020
- Altering the core runtime library
2121

2222
If you encounter bugs or missing functionality in other projects, you MUST:

‎.claude/agents/pr-code-reviewer.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ You are an expert code reviewer specializing in Entity Framework Core extensions
3434

3535
4. **Critical Pattern Verification**
3636
- **StoreObjectIdentifier Usage**: Confirm `GetColumnName(storeIdentifier)` is used for column name resolution to support naming conventions
37-
- **Quote Escaping**: Verify `isDesignTime` parameter is correctly passed to SQL generators
37+
- **Generator Split**: Verify runtime SQL lives in `Generators/[Feature]SqlGenerator.cs` and design-time output in `Design/Generators/[Feature]CSharpGenerator.cs`; identifiers use `SqlBuilderHelper` (`Regclass`/`QualifiedIdentifier`/`QuoteIdentifier`)
38+
- **Migration Extensions**: Confirm `MigrationExtensions/[Feature]MigrationExtensions.cs` adds the operation to `migrationBuilder.Operations`
39+
- **Diff Context**: Verify differs accept `FeatureDiffContext` and resolve renames via it
3840
- **Annotation Storage**: Check that feature metadata uses centralized annotation constants
3941
- **Default Values**: Ensure `DefaultValues.cs` constants are referenced instead of hardcoded values
40-
- **Continuous Aggregate Encoding**: Validate colon-delimited aggregate function strings follow the correct format
42+
- **Continuous Aggregate Encoding**: Validate `ContinuousAggregateFunction` values and the colon-delimited annotation format follow the correct format
4143

4244
5. **Project Structure Compliance**
4345
- Verify files are in correct namespaces and directories

0 commit comments

Comments
 (0)