Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 55 additions & 11 deletions src/Quarry.Generator/Generation/InterceptorCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static partial class InterceptorCodeGenerator
public static string GenerateInterceptorsFile(
string contextClassName,
string? contextNamespace,
string fileTag,
IReadOnlyList<UsageSiteInfo> usageSites,
IReadOnlyList<PrebuiltChainInfo>? prebuiltChains = null)
{
Expand Down Expand Up @@ -99,7 +100,7 @@ public static string GenerateInterceptorsFile(
sb.AppendLine($"/// <summary>");
sb.AppendLine($"/// Generated interceptors for {contextClassName} query methods.");
sb.AppendLine($"/// </summary>");
sb.AppendLine($"file static class {contextClassName}Interceptors");
sb.AppendLine($"file static class {contextClassName}Interceptors_{fileTag}");
sb.AppendLine("{");

// Build chain analysis lookups for execution interceptor generation
Expand Down Expand Up @@ -171,23 +172,66 @@ public static string GenerateInterceptorsFile(
sb.AppendLine();
}

// Generate interceptor methods grouped by kind.
// Include non-analyzable sites that are part of analyzed chains (conditional clause sites).
var groupedSites = usageSites
// Build the filtered list of sites for generation
var allSitesForGeneration = usageSites
.Where(s => s.IsAnalyzable || chainMemberIds.Contains(s.UniqueId))
.GroupBy(s => s.Kind)
.OrderBy(g => g.Key);
.ToList();

// Group interceptors by chain (execution terminal → all clause sites in that chain)
// Sites not part of any chain go into a "Standalone" group
var processedSiteIds = new HashSet<string>();
var chainGroups = new List<(string Label, List<UsageSiteInfo> Sites)>();
var siteByUniqueId = allSitesForGeneration.ToDictionary(s => s.UniqueId);

foreach (var group in groupedSites)
if (prebuiltChains != null)
{
sb.AppendLine($" #region {group.Key} Interceptors");
sb.AppendLine();
foreach (var chain in prebuiltChains)
{
var chainSites = new List<UsageSiteInfo>();
// Add clause sites in chain order
foreach (var clause in chain.Analysis.Clauses)
{
if (siteByUniqueId.TryGetValue(clause.Site.UniqueId, out var matchingSite))
{
chainSites.Add(matchingSite);
processedSiteIds.Add(matchingSite.UniqueId);
}
}
// Add execution site
if (siteByUniqueId.TryGetValue(chain.Analysis.ExecutionSite.UniqueId, out var execSite))
{
chainSites.Add(execSite);
processedSiteIds.Add(execSite.UniqueId);
}

foreach (var site in group)
if (chainSites.Count > 0)
{
var execMethod = chain.Analysis.ExecutionSite.MethodName;
var label = $"Chain: {execMethod} at line {chain.Analysis.ExecutionSite.Line}";
chainGroups.Add((label, chainSites));
}
}
}

// Remaining sites not part of any chain
var standaloneSites = allSitesForGeneration
.Where(s => !processedSiteIds.Contains(s.UniqueId))
.ToList();

if (standaloneSites.Count > 0)
{
chainGroups.Add(("Standalone Interceptors", standaloneSites));
}

// Generate grouped output
foreach (var (label, sites) in chainGroups)
{
sb.AppendLine($" #region {label}");
sb.AppendLine();
foreach (var site in sites)
{
GenerateInterceptorMethod(sb, site, staticFields, chainLookup, clauseBitMap, chainClauseLookup, firstClauseIds);
}

sb.AppendLine($" #endregion");
sb.AppendLine();
}
Expand Down
54 changes: 51 additions & 3 deletions src/Quarry.Generator/Models/ChainAnalysisResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;

namespace Quarry.Generators.Models;
Expand Down Expand Up @@ -73,7 +74,7 @@ internal enum BranchKind
/// Result of analyzing a query chain's control flow from declaration to execution.
/// Produced by <see cref="Quarry.Generators.Parsing.ChainAnalyzer"/>.
/// </summary>
internal sealed class ChainAnalysisResult
internal sealed class ChainAnalysisResult : IEquatable<ChainAnalysisResult>
{
public ChainAnalysisResult(
OptimizationTier tier,
Expand Down Expand Up @@ -129,12 +130,32 @@ public ChainAnalysisResult(
/// Non-null when the chain contains such methods — execution interceptors should be skipped.
/// </summary>
public IReadOnlyList<string>? UnmatchedMethodNames { get; }

public bool Equals(ChainAnalysisResult? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Tier == other.Tier
&& NotAnalyzableReason == other.NotAnalyzableReason
&& EqualityHelpers.SequenceEqual(Clauses, other.Clauses)
&& ExecutionSite.Equals(other.ExecutionSite)
&& EqualityHelpers.SequenceEqual(ConditionalClauses, other.ConditionalClauses)
&& EqualityHelpers.SequenceEqual(PossibleMasks, other.PossibleMasks)
&& EqualityHelpers.NullableSequenceEqual(UnmatchedMethodNames, other.UnmatchedMethodNames);
}

public override bool Equals(object? obj) => Equals(obj as ChainAnalysisResult);

public override int GetHashCode()
{
return HashCode.Combine(Tier, Clauses.Count, ConditionalClauses.Count, PossibleMasks.Count);
}
}

/// <summary>
/// A clause site within an analyzed query chain.
/// </summary>
internal sealed class ChainedClauseSite
internal sealed class ChainedClauseSite : IEquatable<ChainedClauseSite>
{
public ChainedClauseSite(
UsageSiteInfo site,
Expand Down Expand Up @@ -167,12 +188,26 @@ public ChainedClauseSite(
/// Gets the role this clause plays in the query.
/// </summary>
public ClauseRole Role { get; }

public bool Equals(ChainedClauseSite? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return IsConditional == other.IsConditional
&& BitIndex == other.BitIndex
&& Role == other.Role
&& Site.Equals(other.Site);
}

public override bool Equals(object? obj) => Equals(obj as ChainedClauseSite);

public override int GetHashCode() => HashCode.Combine(IsConditional, BitIndex, Role);
}

/// <summary>
/// A conditional clause with its assigned bit index and branch classification.
/// </summary>
internal sealed class ConditionalClause
internal sealed class ConditionalClause : IEquatable<ConditionalClause>
{
public ConditionalClause(
int bitIndex,
Expand All @@ -198,4 +233,17 @@ public ConditionalClause(
/// Gets the branch classification for this conditional clause.
/// </summary>
public BranchKind BranchKind { get; }

public bool Equals(ConditionalClause? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return BitIndex == other.BitIndex
&& BranchKind == other.BranchKind
&& Site.Equals(other.Site);
}

public override bool Equals(object? obj) => Equals(obj as ConditionalClause);

public override int GetHashCode() => HashCode.Combine(BitIndex, BranchKind);
}
80 changes: 76 additions & 4 deletions src/Quarry.Generator/Models/ClauseInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Quarry.Generators.Translation;

Expand All @@ -7,7 +8,7 @@ namespace Quarry.Generators.Models;
/// Represents the analyzed result of a clause expression (Where, OrderBy, GroupBy, Having, Set).
/// Contains the SQL fragment and parameter information for code generation.
/// </summary>
internal class ClauseInfo
internal class ClauseInfo : IEquatable<ClauseInfo>
{
public ClauseInfo(
ClauseKind kind,
Expand Down Expand Up @@ -63,12 +64,30 @@ public static ClauseInfo Failure(ClauseKind kind, string error)
{
return new ClauseInfo(kind, string.Empty, System.Array.Empty<ParameterInfo>(), isSuccess: false, errorMessage: error);
}

public bool Equals(ClauseInfo? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Kind == other.Kind
&& SqlFragment == other.SqlFragment
&& IsSuccess == other.IsSuccess
&& ErrorMessage == other.ErrorMessage
&& EqualityHelpers.SequenceEqual(Parameters, other.Parameters);
}

public override bool Equals(object? obj) => Equals(obj as ClauseInfo);

public override int GetHashCode()
{
return HashCode.Combine(Kind, SqlFragment, IsSuccess, Parameters.Count);
}
}

/// <summary>
/// Represents information about an OrderBy clause, including column and direction.
/// </summary>
internal sealed class OrderByClauseInfo : ClauseInfo
internal sealed class OrderByClauseInfo : ClauseInfo, IEquatable<OrderByClauseInfo>
{
public OrderByClauseInfo(
string columnSql,
Expand All @@ -89,12 +108,28 @@ public OrderByClauseInfo(
/// Gets whether the order is descending.
/// </summary>
public bool IsDescending { get; }

public bool Equals(OrderByClauseInfo? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return ColumnSql == other.ColumnSql
&& IsDescending == other.IsDescending
&& base.Equals(other);
}

public override bool Equals(object? obj) => Equals(obj as OrderByClauseInfo);

public override int GetHashCode()
{
return HashCode.Combine(Kind, ColumnSql, IsDescending);
}
}

/// <summary>
/// Represents information about a Set clause for Update operations.
/// </summary>
internal sealed class SetClauseInfo : ClauseInfo
internal sealed class SetClauseInfo : ClauseInfo, IEquatable<SetClauseInfo>
{
public SetClauseInfo(
string columnSql,
Expand Down Expand Up @@ -123,12 +158,29 @@ public SetClauseInfo(
/// When set, the value should be wrapped with ToDb() before binding.
/// </summary>
public string? CustomTypeMappingClass { get; }

public bool Equals(SetClauseInfo? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return ColumnSql == other.ColumnSql
&& ParameterIndex == other.ParameterIndex
&& CustomTypeMappingClass == other.CustomTypeMappingClass
&& base.Equals(other);
}

public override bool Equals(object? obj) => Equals(obj as SetClauseInfo);

public override int GetHashCode()
{
return HashCode.Combine(Kind, ColumnSql, ParameterIndex);
}
}

/// <summary>
/// Represents information about a Join clause.
/// </summary>
internal sealed class JoinClauseInfo : ClauseInfo
internal sealed class JoinClauseInfo : ClauseInfo, IEquatable<JoinClauseInfo>
{
public JoinClauseInfo(
JoinClauseKind joinKind,
Expand Down Expand Up @@ -177,6 +229,26 @@ public JoinClauseInfo(
/// Gets the alias for the joined table (e.g., "t1"), or null if no alias.
/// </summary>
public string? TableAlias { get; }

public bool Equals(JoinClauseInfo? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return JoinKind == other.JoinKind
&& JoinedEntityName == other.JoinedEntityName
&& JoinedTableName == other.JoinedTableName
&& OnConditionSql == other.OnConditionSql
&& JoinedSchemaName == other.JoinedSchemaName
&& TableAlias == other.TableAlias
&& base.Equals(other);
}

public override bool Equals(object? obj) => Equals(obj as JoinClauseInfo);

public override int GetHashCode()
{
return HashCode.Combine(Kind, JoinKind, JoinedEntityName, JoinedTableName);
}
}

/// <summary>
Expand Down
Loading
Loading