Skip to content

Commit 8cf7015

Browse files
author
MPCoreDeveloper
committed
post test
1 parent 8d4cc09 commit 8cf7015

15 files changed

+267
-68
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## General Guidelines
44
- Test programs should be located in the `tests` folder and not in the repository root.
55
- All project documentation must be written in English. This includes docs, README files, technical specs, implementation plans, and code comments.
6+
- Provide periodic progress updates while work is ongoing to ensure the assistant is not stuck.
67

78
## Code Style
89
- Use specific formatting rules
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
2+
3+
namespace SharpCoreDB.EntityFrameworkCore.Infrastructure;
4+
5+
/// <summary>
6+
/// Convention set builder for SharpCoreDB.
7+
/// Ensures relational conventions (including RelationalModelConvention for table-entity mappings)
8+
/// are properly added to the model building pipeline.
9+
/// </summary>
10+
public class SharpCoreDBConventionSetBuilder(
11+
ProviderConventionSetBuilderDependencies dependencies,
12+
RelationalConventionSetBuilderDependencies relationalDependencies)
13+
: RelationalConventionSetBuilder(dependencies, relationalDependencies)
14+
{
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Infrastructure;
3+
using SharpCoreDB.EntityFrameworkCore.Query;
4+
5+
namespace SharpCoreDB.EntityFrameworkCore.Infrastructure;
6+
7+
/// <summary>
8+
/// Customizes the EF Core model for SharpCoreDB.
9+
/// </summary>
10+
public sealed class SharpCoreDBModelCustomizer(ModelCustomizerDependencies dependencies)
11+
: RelationalModelCustomizer(dependencies)
12+
{
13+
/// <inheritdoc />
14+
public override void Customize(ModelBuilder modelBuilder, DbContext context)
15+
{
16+
base.Customize(modelBuilder, context);
17+
18+
modelBuilder
19+
.HasDbFunction(typeof(SharpCoreDBDbFunctionsExtensions)
20+
.GetMethod(nameof(SharpCoreDBDbFunctionsExtensions.GraphTraverse))!)
21+
.HasName("GRAPH_TRAVERSE");
22+
}
23+
}

src/SharpCoreDB.EntityFrameworkCore/Infrastructure/SharpCoreDBModelRuntimeInitializer.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@ namespace SharpCoreDB.EntityFrameworkCore.Infrastructure;
77

88
/// <summary>
99
/// Model runtime initializer for SharpCoreDB.
10-
/// Forces eager relational model creation during initialization to ensure
11-
/// column-property mappings reference the RuntimeEntityType instances
12-
/// from the RuntimeModel (not the design-time EntityType instances).
13-
/// Without this, lazy creation via GetRelationalModel() may use the
14-
/// design-time model, causing entity type reference mismatches in queries.
10+
/// Ensures the relational model is properly created from RuntimeEntityType instances
11+
/// so that table mappings reference the correct entity type objects during query compilation.
1512
/// </summary>
1613
public class SharpCoreDBModelRuntimeInitializer(
1714
ModelRuntimeInitializerDependencies dependencies,
@@ -24,16 +21,9 @@ public override IModel Initialize(
2421
bool designTime = true,
2522
IDiagnosticsLogger<DbLoggerCategory.Model.Validation>? validationLogger = null)
2623
{
27-
// Pass designTime=false to the base to prevent it from creating the relational
28-
// model using the design-time model's entity types (which causes reference mismatches
29-
// with the RuntimeModel's RuntimeEntityType instances during query compilation).
30-
model = base.Initialize(model, designTime: false, validationLogger);
31-
32-
// Now force relational model creation on the RuntimeModel.
33-
// GetRelationalModel() will use GetOrAddRuntimeAnnotationValue to lazily create
34-
// the relational model from the RuntimeModel's entity types.
35-
model.GetRelationalModel();
36-
37-
return model;
24+
// Let the base handle model finalization and relational model creation.
25+
// Pass designTime=true so that RelationalModelRuntimeInitializer eagerly
26+
// creates the relational model with correct entity type references.
27+
return base.Initialize(model, designTime: true, validationLogger);
3828
}
3929
}

src/SharpCoreDB.EntityFrameworkCore/Query/GraphTraversalMethodCallTranslator.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public sealed class GraphTraversalMethodCallTranslator(ISqlExpressionFactory sql
4545
typeof(GraphTraversalQueryableExtensions)
4646
.GetMethod(nameof(GraphTraversalQueryableExtensions.TraverseWhere))!;
4747

48+
private static readonly MethodInfo _graphTraverseFunction =
49+
typeof(SharpCoreDBDbFunctionsExtensions)
50+
.GetMethod(nameof(SharpCoreDBDbFunctionsExtensions.GraphTraverse),
51+
[typeof(long), typeof(string), typeof(int), typeof(GraphTraversalStrategy)])!;
52+
4853
/// <inheritdoc />
4954
public SqlExpression? Translate(
5055
SqlExpression? instance,
@@ -55,23 +60,40 @@ public sealed class GraphTraversalMethodCallTranslator(ISqlExpressionFactory sql
5560
ArgumentNullException.ThrowIfNull(method);
5661
ArgumentNullException.ThrowIfNull(arguments);
5762

63+
// Handle DbFunctions.GraphTraverse(startId, relationshipColumn, maxDepth, strategy)
64+
if (method == _graphTraverseFunction && arguments.Count == 4)
65+
{
66+
var startNodeId = arguments[0];
67+
var relationshipColumn = arguments[1];
68+
var maxDepth = arguments[2];
69+
var strategyArg = arguments[3];
70+
71+
if (strategyArg is not SqlConstantExpression { Value: GraphTraversalStrategy strategy })
72+
{
73+
return null;
74+
}
75+
76+
return _sqlExpressionFactory.Function(
77+
"GRAPH_TRAVERSE",
78+
arguments: [startNodeId, relationshipColumn, maxDepth, new SqlConstantExpression(System.Linq.Expressions.Expression.Constant((int)strategy), null)],
79+
nullable: false,
80+
argumentsPropagateNullability: [false, false, false, false],
81+
returnType: typeof(long));
82+
}
83+
5884
// Handle: .Traverse(startId, relationshipColumn, maxDepth, strategy)
5985
if (IsGenericMethodMatch(method, _traverseMethod) && arguments.Count == 5)
6086
{
61-
// arguments[0] is the IQueryable source (skip it)
6287
var startNodeId = arguments[1];
6388
var relationshipColumn = arguments[2];
6489
var maxDepth = arguments[3];
6590
var strategyArg = arguments[4];
6691

67-
// Extract strategy value
6892
if (strategyArg is not SqlConstantExpression { Value: GraphTraversalStrategy strategy })
6993
{
70-
// Log and skip translation if strategy is not constant
7194
return null;
7295
}
7396

74-
// Create GRAPH_TRAVERSE(startId, relationshipColumn, maxDepth, strategy) SQL function call
7597
return _sqlExpressionFactory.Function(
7698
"GRAPH_TRAVERSE",
7799
arguments: [startNodeId, relationshipColumn, maxDepth, new SqlConstantExpression(System.Linq.Expressions.Expression.Constant((int)strategy), null)],
@@ -83,23 +105,19 @@ public sealed class GraphTraversalMethodCallTranslator(ISqlExpressionFactory sql
83105
// Handle: .WhereIn(traversalIds)
84106
if (IsGenericMethodMatch(method, _whereInMethod) && arguments.Count == 2)
85107
{
86-
// This is translated by EF Core's standard IN handling
87-
// Just pass through for now
88108
return null;
89109
}
90110

91111
// Handle: .TraverseWhere(...)
92112
if (IsGenericMethodMatch(method, _traverseWhereMethod) && arguments.Count == 6)
93113
{
94-
// arguments[0] = source, [1] = startId, [2] = column, [3] = depth, [4] = strategy, [5] = predicate
95114
var startNodeId = arguments[1];
96115
var relationshipColumn = arguments[2];
97116
var maxDepth = arguments[3];
98117
var strategyArg = arguments[4];
99118

100119
if (strategyArg is not SqlConstantExpression { Value: GraphTraversalStrategy strategy })
101120
{
102-
// Log and skip translation if strategy is not constant
103121
return null;
104122
}
105123

src/SharpCoreDB.EntityFrameworkCore/Query/GraphTraversalQueryableExtensions.cs

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,7 @@ public static IQueryable<long> Traverse<TEntity>(
5959
if (maxDepth < 0)
6060
throw new ArgumentOutOfRangeException(nameof(maxDepth), "Max depth must be non-negative");
6161

62-
var methodInfo = typeof(GraphTraversalQueryableExtensions)
63-
.GetMethod(nameof(Traverse),
64-
[typeof(IQueryable<>).MakeGenericType(typeof(TEntity)),
65-
typeof(long), typeof(string), typeof(int), typeof(GraphTraversalStrategy)])!
66-
.MakeGenericMethod(typeof(TEntity));
67-
68-
var methodCall = Expression.Call(
69-
methodInfo,
70-
source.Expression,
71-
Expression.Constant(startNodeId),
72-
Expression.Constant(relationshipColumn),
73-
Expression.Constant(maxDepth),
74-
Expression.Constant(strategy));
75-
76-
return source.Provider.CreateQuery<long>(methodCall);
62+
return source.Select(_ => SharpCoreDBDbFunctionsExtensions.GraphTraverse(startNodeId, relationshipColumn, maxDepth, strategy));
7763
}
7864

7965
/// <summary>
@@ -163,23 +149,21 @@ public static IQueryable<TEntity> TraverseWhere<TEntity>(
163149
ArgumentNullException.ThrowIfNull(source);
164150
ArgumentNullException.ThrowIfNull(predicate);
165151

166-
// First get traversal results
167152
var traversalIds = source.Traverse(startNodeId, relationshipColumn, maxDepth, strategy);
168153

169-
// Then apply additional filter
170154
var parameter = predicate.Parameters[0];
171155
var idProperty = typeof(TEntity).GetProperty("Id")
172156
?? throw new InvalidOperationException($"Entity {typeof(TEntity).Name} must have an 'Id' property");
173157

174158
var propertyAccess = Expression.Property(parameter, idProperty);
175-
var idList = Expression.Constant(traversalIds.ToList());
176-
var containsMethod = typeof(List<long>).GetMethod("Contains", [typeof(long)])!;
177-
var inClause = Expression.Call(idList, containsMethod, propertyAccess);
159+
var containsMethod = typeof(Queryable)
160+
.GetMethods()
161+
.First(m => m.Name == nameof(Queryable.Contains) && m.GetParameters().Length == 2)
162+
.MakeGenericMethod(typeof(long));
163+
var inClause = Expression.Call(null, containsMethod, traversalIds.Expression, propertyAccess);
178164

179-
// Combine: WHERE (...traversal...) AND (user predicate)
180-
var combined = Expression.Lambda<Func<TEntity, bool>>(
181-
Expression.AndAlso(inClause, predicate.Body),
182-
parameter);
165+
var combinedBody = Expression.AndAlso(inClause, predicate.Body);
166+
var combined = Expression.Lambda<Func<TEntity, bool>>(combinedBody, parameter);
183167

184168
return source.Where(combined);
185169
}
@@ -238,4 +222,23 @@ public static GraphTraversalQueryable<TEntity> GraphTraverse<TEntity>(
238222

239223
return new GraphTraversalQueryable<TEntity>(source, startNodeId, relationshipColumn, maxDepth);
240224
}
225+
226+
/// <summary>
227+
/// Limits the number of results from a graph traversal query.
228+
/// Validates that count is non-negative before applying the Take operation.
229+
/// </summary>
230+
/// <param name="source">The traversal query source.</param>
231+
/// <param name="count">The maximum number of elements to return.</param>
232+
/// <returns>A queryable with at most count elements.</returns>
233+
/// <exception cref="ArgumentNullException">Thrown when source is null.</exception>
234+
/// <exception cref="ArgumentOutOfRangeException">Thrown when count is negative.</exception>
235+
public static IQueryable<long> Take(this IQueryable<long> source, int count)
236+
{
237+
ArgumentNullException.ThrowIfNull(source);
238+
239+
if (count < 0)
240+
throw new ArgumentOutOfRangeException(nameof(count), "Count must be non-negative");
241+
242+
return Queryable.Take(source, count);
243+
}
241244
}

src/SharpCoreDB.EntityFrameworkCore/Query/SharpCoreDBCollateTranslator.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.EntityFrameworkCore.Diagnostics;
33
using Microsoft.EntityFrameworkCore.Query;
44
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
5+
using SharpCoreDB.Interfaces;
56
using System.Linq.Expressions;
67
using System.Reflection;
78

@@ -102,4 +103,23 @@ public static string Collate(
102103
throw new InvalidOperationException(
103104
$"{nameof(Collate)} is a database function and can only be used in LINQ to Entities queries.");
104105
}
106+
107+
/// <summary>
108+
/// Translates to GRAPH_TRAVERSE(startNodeId, relationshipColumn, maxDepth, strategy).
109+
/// </summary>
110+
/// <param name="startNodeId">Starting node ID.</param>
111+
/// <param name="relationshipColumn">ROWREF column name.</param>
112+
/// <param name="maxDepth">Maximum traversal depth.</param>
113+
/// <param name="strategy">Traversal strategy.</param>
114+
/// <returns>Traversal result row ID.</returns>
115+
[DbFunction("GRAPH_TRAVERSE", IsBuiltIn = true)]
116+
public static long GraphTraverse(
117+
long startNodeId,
118+
string relationshipColumn,
119+
int maxDepth,
120+
GraphTraversalStrategy strategy)
121+
{
122+
throw new InvalidOperationException(
123+
$"{nameof(GraphTraverse)} is a database function and can only be used in LINQ to Entities queries.");
124+
}
105125
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.EntityFrameworkCore.Query;
2+
using SharpCoreDB.Interfaces;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
6+
namespace SharpCoreDB.EntityFrameworkCore.Query;
7+
8+
/// <summary>
9+
/// Prevents EF Core from pre-evaluating SharpCoreDB DbFunctions during query translation.
10+
/// </summary>
11+
public sealed class SharpCoreDBEvaluatableExpressionFilterPlugin : IEvaluatableExpressionFilterPlugin
12+
{
13+
private static readonly MethodInfo _graphTraverseMethod =
14+
typeof(SharpCoreDBDbFunctionsExtensions)
15+
.GetMethod(nameof(SharpCoreDBDbFunctionsExtensions.GraphTraverse),
16+
new[] { typeof(long), typeof(string), typeof(int), typeof(GraphTraversalStrategy) })!;
17+
18+
/// <inheritdoc />
19+
public bool IsEvaluatableExpression(Expression expression)
20+
{
21+
return expression is MethodCallExpression methodCall
22+
&& methodCall.Method == _graphTraverseMethod
23+
? false
24+
: true;
25+
}
26+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Microsoft.EntityFrameworkCore.Query;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
5+
namespace SharpCoreDB.EntityFrameworkCore.Query;
6+
7+
/// <summary>
8+
/// Rewrites SharpCoreDB-specific LINQ methods into translatable query patterns.
9+
/// </summary>
10+
public sealed class SharpCoreDBQueryTranslationPreprocessor(
11+
QueryTranslationPreprocessorDependencies dependencies,
12+
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
13+
QueryCompilationContext queryCompilationContext)
14+
: RelationalQueryTranslationPreprocessor(dependencies, relationalDependencies, queryCompilationContext)
15+
{
16+
/// <inheritdoc />
17+
public override Expression Process(Expression query)
18+
{
19+
var rewritten = new TraverseMethodRewritingVisitor().Visit(query);
20+
return base.Process(rewritten);
21+
}
22+
23+
private sealed class TraverseMethodRewritingVisitor : ExpressionVisitor
24+
{
25+
private static readonly MethodInfo _traverseMethod =
26+
typeof(GraphTraversalQueryableExtensions)
27+
.GetMethods()
28+
.First(m => m.Name == nameof(GraphTraversalQueryableExtensions.Traverse)
29+
&& m.GetParameters().Length == 5);
30+
31+
private static readonly MethodInfo _selectMethod =
32+
typeof(Queryable)
33+
.GetMethods()
34+
.First(m => m.Name == nameof(Queryable.Select)
35+
&& m.GetParameters().Length == 2);
36+
37+
protected override Expression VisitMethodCall(MethodCallExpression node)
38+
{
39+
if (node.Method.IsGenericMethod
40+
&& node.Method.GetGenericMethodDefinition() == _traverseMethod)
41+
{
42+
return RewriteTraverse(node);
43+
}
44+
45+
return base.VisitMethodCall(node);
46+
}
47+
48+
private static Expression RewriteTraverse(MethodCallExpression node)
49+
{
50+
var source = node.Arguments[0];
51+
var elementType = source.Type.GetGenericArguments().First();
52+
var parameter = Expression.Parameter(elementType, "e");
53+
54+
var graphTraverseCall = Expression.Call(
55+
typeof(SharpCoreDBDbFunctionsExtensions),
56+
nameof(SharpCoreDBDbFunctionsExtensions.GraphTraverse),
57+
Type.EmptyTypes,
58+
node.Arguments[1],
59+
node.Arguments[2],
60+
node.Arguments[3],
61+
node.Arguments[4]);
62+
63+
var selector = Expression.Lambda(graphTraverseCall, parameter);
64+
var selectMethod = _selectMethod.MakeGenericMethod(elementType, typeof(long));
65+
66+
return Expression.Call(null, selectMethod, source, selector);
67+
}
68+
}
69+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.EntityFrameworkCore.Query;
2+
3+
namespace SharpCoreDB.EntityFrameworkCore.Query;
4+
5+
/// <summary>
6+
/// Factory for SharpCoreDB query translation preprocessors.
7+
/// </summary>
8+
public sealed class SharpCoreDBQueryTranslationPreprocessorFactory(
9+
QueryTranslationPreprocessorDependencies dependencies,
10+
RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
11+
: IQueryTranslationPreprocessorFactory
12+
{
13+
/// <inheritdoc />
14+
public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
15+
=> new SharpCoreDBQueryTranslationPreprocessor(
16+
dependencies,
17+
relationalDependencies,
18+
queryCompilationContext);
19+
}

0 commit comments

Comments
 (0)