Skip to content

Commit 5bb56ca

Browse files
committed
Reorganize trunk files into partials and subdirectories: split Selection.Execution.cs (1173 lines) into trunk + SetOps/Aggregate/Window/Joins/OrderBy phase partials, extract SqlType.Promote (and PromoteForArithmetic) into SqlType.Promote.cs, and group the Simulation.* and SimulatedSqlException.* root partials into Simulation/ and Errors/ subdirectories; pure code organization, no behavior changes.
1 parent 3dfec88 commit 5bb56ca

27 files changed

Lines changed: 1298 additions & 1218 deletions

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ If `obj/` permission errors appear (e.g. `Up2Date` access denied), the user has
4343
Top-level layout (`SqlServerSimulator/`):
4444
- `Storage/` — pages, types, row encoder/decoder, heap, constraints
4545
- `Parser/` — tokenizer, expressions, query planning + execution
46-
- root — `Simulated*` ADO.NET implementations, `Simulation`
46+
- `Simulation/``Simulation` partial-class root and per-statement-kind partials (`Simulation.Create.cs`, `Simulation.Insert.cs`, etc.)
47+
- `Errors/``SimulatedSqlException` partial-class root and topical factory partials (`SimulatedSqlException.TypeErrors.cs`, etc.)
48+
- root — `Simulated*` ADO.NET implementations
4749

4850
### Storage
4951

SqlServerSimulator/SimulatedSqlException.ConstraintErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.ConstraintErrors.cs

File renamed without changes.

SqlServerSimulator/SimulatedSqlException.QueryErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.QueryErrors.cs

File renamed without changes.

SqlServerSimulator/SimulatedSqlException.ResolutionErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.ResolutionErrors.cs

File renamed without changes.

SqlServerSimulator/SimulatedSqlException.SchemaErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.SchemaErrors.cs

File renamed without changes.

SqlServerSimulator/SimulatedSqlException.SyntaxErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.SyntaxErrors.cs

File renamed without changes.

SqlServerSimulator/SimulatedSqlException.TypeErrors.cs renamed to SqlServerSimulator/Errors/SimulatedSqlException.TypeErrors.cs

File renamed without changes.
File renamed without changes.
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using SqlServerSimulator.Parser.Expressions;
2+
using SqlServerSimulator.Storage;
3+
4+
namespace SqlServerSimulator.Parser;
5+
6+
/// <summary>
7+
/// Aggregate-mode executor for SELECTs that carry aggregates, GROUP BY, or
8+
/// HAVING. Streams every input tuple through each projection aggregate's
9+
/// accumulator (per group when GROUP BY is in play), then projects one
10+
/// output row per group.
11+
/// </summary>
12+
internal sealed partial class Selection
13+
{
14+
/// <summary>
15+
/// Aggregate-mode executor: streams every input tuple through each
16+
/// projection aggregate's accumulator (per group when GROUP BY is in
17+
/// play), then projects one output row per group. WHERE excluders run
18+
/// per source row before aggregation; HAVING runs per group after
19+
/// finalization; ORDER BY runs across groups at the end. Without
20+
/// GROUP BY the output is exactly one row even for empty input (SQL
21+
/// Server's implicit-empty-GROUP-BY rule); per-aggregate empty-input
22+
/// behavior is each aggregator's responsibility (COUNT returns 0;
23+
/// everything else NULL). <paramref name="outerResolver"/> chains
24+
/// unresolved column references to the enclosing scope.
25+
/// </summary>
26+
private static List<byte[]> BuildAggregateProjectionRows(
27+
FromSource[] sources,
28+
JoinSpec[] joins,
29+
Func<MultiPartName, SqlType> resolveColumnType,
30+
List<Expression> expressions,
31+
FromClause fromClause,
32+
SqlType[] outputSchema,
33+
List<AggregateExpression> aggregates,
34+
int? topCount,
35+
int? offsetCount,
36+
int? fetchCount,
37+
Func<MultiPartName, SqlValue>? outerResolver)
38+
{
39+
if (topCount == 0)
40+
return [];
41+
42+
var groupByExpressions = fromClause.GroupBy;
43+
var groupByCount = groupByExpressions.Count;
44+
var groups = new Dictionary<SqlValueKey, GroupState>();
45+
46+
var aggregateOperandTypes = new SqlType[aggregates.Count];
47+
var aggregateResultTypes = new SqlType[aggregates.Count];
48+
for (var i = 0; i < aggregates.Count; i++)
49+
{
50+
aggregateOperandTypes[i] = aggregates[i].Operand?.GetSqlType(resolveColumnType) ?? SqlType.Int32;
51+
aggregateResultTypes[i] = aggregates[i].GetSqlType(resolveColumnType);
52+
}
53+
54+
GroupState NewGroup()
55+
{
56+
var freshAggregators = new Aggregator[aggregates.Count];
57+
for (var i = 0; i < aggregates.Count; i++)
58+
freshAggregators[i] = Aggregator.Create(aggregates[i], aggregateOperandTypes[i], aggregateResultTypes[i]);
59+
return new(keyValues: new SqlValue[groupByCount], aggregators: freshAggregators);
60+
}
61+
62+
if (groupByCount == 0)
63+
groups[SqlValueKey.Empty] = NewGroup();
64+
65+
foreach (var tuple in EnumerateJoinedRows(sources, joins, outerResolver))
66+
{
67+
var localTuple = tuple;
68+
SqlValue ResolveColumn(MultiPartName name) => ResolveAcrossTuple(sources, localTuple, name, outerResolver, ResolveColumn);
69+
70+
var include = true;
71+
foreach (var excluder in fromClause.Excluders)
72+
{
73+
if (excluder.Run(ResolveColumn) != true)
74+
{
75+
include = false;
76+
break;
77+
}
78+
}
79+
if (!include)
80+
continue;
81+
82+
GroupState state;
83+
if (groupByCount == 0)
84+
{
85+
state = groups[SqlValueKey.Empty];
86+
}
87+
else
88+
{
89+
var keyValues = new SqlValue[groupByCount];
90+
for (var i = 0; i < groupByCount; i++)
91+
keyValues[i] = groupByExpressions[i].Run(ResolveColumn);
92+
var key = new SqlValueKey(keyValues);
93+
if (!groups.TryGetValue(key, out state!))
94+
{
95+
state = NewGroup();
96+
Array.Copy(keyValues, state.KeyValues, groupByCount);
97+
groups[key] = state;
98+
}
99+
}
100+
101+
for (var i = 0; i < aggregates.Count; i++)
102+
{
103+
var aggregate = aggregates[i];
104+
if (aggregate.Kind == AggregateKind.StringAgg && state.Aggregators[i] is Aggregators.StringAggAggregator stringAgg)
105+
{
106+
var separatorValue = aggregate.Separator!.Run(ResolveColumn);
107+
stringAgg.SetSeparator(separatorValue.IsNull ? string.Empty : separatorValue.AsString);
108+
}
109+
var operand = aggregate.Operand;
110+
state.Aggregators[i].Add(operand is null ? SqlValue.Null(SqlType.Int32) : operand.Run(ResolveColumn));
111+
}
112+
}
113+
114+
var output = new List<byte[]>();
115+
foreach (var (_, state) in groups)
116+
{
117+
for (var i = 0; i < aggregates.Count; i++)
118+
aggregates[i].BindResult(state.Aggregators[i].Result());
119+
120+
SqlValue ResolveByGroupKey(MultiPartName name)
121+
{
122+
for (var i = 0; i < groupByCount; i++)
123+
{
124+
if (groupByExpressions[i] is Reference r
125+
&& Collation.Default.Equals(r.Name, name.Leaf))
126+
{
127+
return state.KeyValues[i];
128+
}
129+
}
130+
return outerResolver is not null
131+
? outerResolver(name)
132+
: throw SimulatedSqlException.InvalidColumnName(name);
133+
}
134+
135+
if (fromClause.Having is { } having && having.Run(ResolveByGroupKey) != true)
136+
continue;
137+
138+
var projected = new SqlValue[expressions.Count];
139+
for (var i = 0; i < expressions.Count; i++)
140+
projected[i] = expressions[i].Run(ResolveByGroupKey);
141+
142+
output.Add(RowEncoder.EncodeRow(outputSchema, projected));
143+
}
144+
145+
if (topCount is { } topLimit && output.Count > topLimit)
146+
output = [.. output.Take(topLimit)];
147+
148+
if (offsetCount is { } offset && offset > 0)
149+
output = [.. output.Skip(offset)];
150+
if (fetchCount is { } fetchLimit && output.Count > fetchLimit)
151+
output = [.. output.Take(fetchLimit)];
152+
153+
return output;
154+
}
155+
156+
/// <summary>
157+
/// Per-group state inside <see cref="BuildAggregateProjectionRows"/>: the
158+
/// resolved key tuple (used to populate non-aggregate projection slots
159+
/// from the GROUP BY's column references) plus one aggregator per
160+
/// <see cref="AggregateExpression"/> in the projection.
161+
/// </summary>
162+
private sealed class GroupState(SqlValue[] keyValues, Aggregator[] aggregators)
163+
{
164+
public readonly SqlValue[] KeyValues = keyValues;
165+
public readonly Aggregator[] Aggregators = aggregators;
166+
}
167+
168+
/// <summary>
169+
/// Hash-key wrapper around a <see cref="SqlValue"/> tuple used as a
170+
/// dictionary key for GROUP BY buckets. Two NULL slots compare equal
171+
/// (matching SQL Server: NULL is a valid group key with one bucket).
172+
/// </summary>
173+
private readonly struct SqlValueKey(SqlValue[] values) : IEquatable<SqlValueKey>
174+
{
175+
public static readonly SqlValueKey Empty = new([]);
176+
177+
private readonly SqlValue[] values = values;
178+
179+
public bool Equals(SqlValueKey other)
180+
{
181+
if (this.values.Length != other.values.Length)
182+
return false;
183+
for (var i = 0; i < this.values.Length; i++)
184+
{
185+
var a = this.values[i];
186+
var b = other.values[i];
187+
if (a.IsNull != b.IsNull)
188+
return false;
189+
if (a.IsNull)
190+
continue;
191+
if (!a.Equals(b))
192+
return false;
193+
}
194+
return true;
195+
}
196+
197+
public override bool Equals(object? obj) => obj is SqlValueKey other && Equals(other);
198+
199+
public override int GetHashCode()
200+
{
201+
var h = new HashCode();
202+
foreach (var v in this.values)
203+
h.Add(v.IsNull ? 0 : v.GetHashCode());
204+
return h.ToHashCode();
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)