Skip to content

Commit 6225678

Browse files
authored
Benchmark suite migration: decimal -> double + generator aggregate-type fix (#297)
* [WIP] Quarry.Generator: fix aggregate CLR-type resolution (partial) + session Phase 1 of benchmark-double-migration, suspended before the typed-marker rename per user pushback on the "object" sentinel. What is in this commit: - ProjectionAnalyzer.ResolveAggregateClrType reordered to consult the schema-driven column lookup first, then SemanticModel argument type, then a gated SemanticModel invocation-return-type fallback. - 6 Sum/Avg call sites changed from the bogus "decimal" default to the interim "object" sentinel so ChainAnalyzer.BuildProjection's enrichment pass converts the unresolved type into the real column type. - 5 new tests in AggregateTypeResolutionTests.cs covering Sum over double/decimal/int/long columns and Avg over double — all passing. - Full suite remains green at 3482/3482 (baseline 3477 + 5 new). What remains in Phase 1: - Replace the bare "object" sentinel at the 6 aggregate call sites with a named TypeClassification.UnresolvedTypeMarker constant ("?"), already recognized by both IsUnresolvedTypeName helpers. Rename refactor only; behavior unchanged. See _sessions/benchmark-double-migration/ for the full workflow state. * [WIP] session: record WIP commit hash 892312d and session log entry * Quarry.Generator: complete Phase 1 — typed UnresolvedTypeMarker sentinel Introduces TypeClassification.UnresolvedTypeMarker = "?" as the canonical sentinel for an unresolved aggregate CLR type produced by Stage 1 syntax-only analysis. Replaces the bare "object" magic-string at the 8 Sum/Avg call sites in ProjectionAnalyzer (regular, joined, window, and joined-window aggregate paths) with the named constant, removing the ambiguity between the legitimate "object" CLR type and an unresolved-pending-enrichment marker. Behavior preserving — both IsUnresolvedTypeName helpers already recognize "?". Min/Max defaults remain at "object" (broader follow-up #1 in plan). Pairs with the earlier reorder/gate of ResolveAggregateClrType (WIP 892312d) to close Phase 1 of benchmark-double-migration. All 5 AggregateTypeResolutionTests pass; full suite green (146 + 201 + 3143 = 3490 passed, 0 failed). * Benchmarks: migrate Total/UnitPrice/LineTotal from decimal to double Phase 2 of benchmark-double-migration. Switches the money-shaped columns on OrderSchema/OrderItemSchema and their EF/DTO mirrors from decimal to double, and updates DatabaseSetup seed literals (10.0m/1.5m/5.0m/2.5m → 10.0/1.5/5.0/2.5) accordingly. The DapperOrderLagDto workaround class is removed; the regular OrderLagDto now uses double/double? so Dapper no longer needs a parallel type. This removes the SQLite GetDecimal(string-parse) per-cell tax from every reader on the benchmark hot path, so the inter-library comparison measures library overhead rather than a driver implementation choice. Quarry.Benchmarks does NOT compile in isolation after this commit (the reader bodies still call GetDecimal/ExecuteScalarAsync<decimal> and three benchmark files remain parked as `.cs.disabled`). Phase 3 restores compilation. Quarry.Tests builds and the full test suite still passes. * Benchmarks: update readers and Aggregate/Avg signatures to double Phase 3 of benchmark-double-migration. Restores compilation of Quarry.Benchmarks after the Phase 2 schema/DTO migration: - Replace `reader.GetDecimal(1)` with `reader.GetDouble(1)` across 8 reader benchmarks (CteSimple, CteMulti, CteProjection, ComplexJoinFilterPaginate, JoinInner, JoinThreeTable, WindowLag, WindowRunningSum) — 13 call sites total. - WindowLagBenchmarks.Dapper_Lag now uses the regular OrderLagDto; the DapperOrderLagDto workaround class was removed in Phase 2. - AggregateSumBenchmarks / AggregateAvgBenchmarks switched their Raw/Dapper/EfCore/Quarry/SqlKata methods from `Task<decimal>` / `ExecuteScalarAsync<decimal>` / `Convert.ToDecimal` to `<double>` / `Convert.ToDouble`. Quarry.Benchmarks builds clean. * Benchmarks: remove obsolete GetDecimal documentation Phase 4 of benchmark-double-migration. With the schema now using `double` for Total/UnitPrice/LineTotal, the GetDecimal string-parse cost is no longer on the benchmark hot path and the documentation explaining it is stale. Drops: - The canonical 22-line NOTE block at the top of CteSimpleBenchmarks.cs that documented the SqliteValueReader.GetDecimal implementation, Dapper's IL-emitted indexer path, and Quarry's deliberate refusal to use the (decimal)GetDouble trick. - The 7 cross-reference comment blocks in ComplexJoinFilterPaginate, CteMulti, CteProjection, JoinInner, JoinThreeTable, WindowLag, and WindowRunningSum that pointed at the canonical block. The rationale lives in PR and commit history for anyone who needs it. * Workflow: mark Phase 5 complete, transition to REVIEW Full test suite green in Release (3490 / 3490 passed). Benchmark smoke run executed 30 representative benchmarks across CteSimple, AggregateSum, and WindowLag end-to-end with no failures; Quarry tracks the hand-rolled Raw baseline within ~1% on WindowLag (~132µs each), matching the empirical numbers cited in the problem statement. All 5 implementation phases of benchmark-double-migration are complete; advancing to REVIEW. * REMEDIATE: extend Stage 4 enrichment to joined scalar aggregates Addresses review findings #14 (A, low) and #10 (B, medium): - TypeClassification.UnresolvedTypeMarker: public const → internal const (matches the class's accessibility). - Add 3 unit tests for joined-aggregate, single-entity window-aggregate, and joined-window-aggregate paths to AggregateTypeResolutionTests. Writing the joined tests surfaced a latent bug deeper than the original fix scope: ProjectionAnalyzer.AnalyzeJoinedInvocation (the entry point for `(u, o) => Sql.Sum(o.Total)` scalar joined projections) was constructing the aggregate ProjectedColumn without setting TableAlias. Stage 4 ChainAnalyzer.BuildProjection's aggregate enrichment then called TryResolveAggregateTypeFromSql with a null tableAlias, the alias-keyed lookup in perAliasLookup fell through, and the unresolved marker leaked all the way to the carrier (IJoinedQueryBuilder<User, Order, ?>) and reader Func type — uncompilable. The sibling ResolveJoinedAggregate (used for tuple-element joined projections) already extracts and sets TableAlias correctly. The fix mirrors that logic in AnalyzeJoinedInvocation — pull the alias from the first column argument (o.Total → "t1") and pass it through to the ProjectedColumn constructor. Behavior preserving for all previously-working paths. Joined scalar aggregates over non-decimal columns now compile and produce correctly- typed readers. Full suite: 201 + 146 + 3146 = 3493 / 3493 passed. See _sessions/benchmark-double-migration/review.md and workflow.md decision dated 2026-05-19 for full context. * chore: remove session artifacts before merge
1 parent 08d8323 commit 6225678

18 files changed

Lines changed: 481 additions & 131 deletions

src/Quarry.Benchmarks/Benchmarks/AggregateAvgBenchmarks.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,36 @@ namespace Quarry.Benchmarks.Benchmarks;
1010
public class AggregateAvgBenchmarks : BenchmarkBase
1111
{
1212
[Benchmark(Baseline = true)]
13-
public async Task<decimal> Raw_Avg()
13+
public async Task<double> Raw_Avg()
1414
{
1515
await using var cmd = Connection.CreateCommand();
1616
cmd.CommandText = "SELECT AVG(Total) FROM orders";
1717
var result = await cmd.ExecuteScalarAsync();
18-
return Convert.ToDecimal(result);
18+
return Convert.ToDouble(result);
1919
}
2020

2121
[Benchmark]
22-
public async Task<decimal> Dapper_Avg()
22+
public async Task<double> Dapper_Avg()
2323
{
24-
return await Connection.ExecuteScalarAsync<decimal>("SELECT AVG(Total) FROM orders");
24+
return await Connection.ExecuteScalarAsync<double>("SELECT AVG(Total) FROM orders");
2525
}
2626

2727
[Benchmark]
28-
public async Task<decimal> EfCore_Avg()
28+
public async Task<double> EfCore_Avg()
2929
{
3030
return await EfContext.Orders.AsNoTracking().AverageAsync(o => o.Total);
3131
}
3232

3333
[Benchmark]
34-
public async Task<decimal> Quarry_Avg()
34+
public async Task<double> Quarry_Avg()
3535
{
3636
return await QuarryDb.Orders()
3737
.Select(o => Sql.Avg(o.Total))
38-
.ExecuteScalarAsync<decimal>();
38+
.ExecuteScalarAsync<double>();
3939
}
4040

4141
[Benchmark]
42-
public async Task<decimal> SqlKata_Avg()
42+
public async Task<double> SqlKata_Avg()
4343
{
4444
var query = new Query("orders").AsAverage("Total");
4545
var compiled = SqlKataCompiler.Compile(query);
@@ -51,6 +51,6 @@ public async Task<decimal> SqlKata_Avg()
5151
cmd.Parameters.AddWithValue($"@p{cmd.Parameters.Count}", binding);
5252
}
5353
var result = await cmd.ExecuteScalarAsync();
54-
return Convert.ToDecimal(result);
54+
return Convert.ToDouble(result);
5555
}
5656
}

src/Quarry.Benchmarks/Benchmarks/AggregateSumBenchmarks.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,36 @@ namespace Quarry.Benchmarks.Benchmarks;
1010
public class AggregateSumBenchmarks : BenchmarkBase
1111
{
1212
[Benchmark(Baseline = true)]
13-
public async Task<decimal> Raw_Sum()
13+
public async Task<double> Raw_Sum()
1414
{
1515
await using var cmd = Connection.CreateCommand();
1616
cmd.CommandText = "SELECT SUM(Total) FROM orders";
1717
var result = await cmd.ExecuteScalarAsync();
18-
return Convert.ToDecimal(result);
18+
return Convert.ToDouble(result);
1919
}
2020

2121
[Benchmark]
22-
public async Task<decimal> Dapper_Sum()
22+
public async Task<double> Dapper_Sum()
2323
{
24-
return await Connection.ExecuteScalarAsync<decimal>("SELECT SUM(Total) FROM orders");
24+
return await Connection.ExecuteScalarAsync<double>("SELECT SUM(Total) FROM orders");
2525
}
2626

2727
[Benchmark]
28-
public async Task<decimal> EfCore_Sum()
28+
public async Task<double> EfCore_Sum()
2929
{
3030
return await EfContext.Orders.AsNoTracking().SumAsync(o => o.Total);
3131
}
3232

3333
[Benchmark]
34-
public async Task<decimal> Quarry_Sum()
34+
public async Task<double> Quarry_Sum()
3535
{
3636
return await QuarryDb.Orders()
3737
.Select(o => Sql.Sum(o.Total))
38-
.ExecuteScalarAsync<decimal>();
38+
.ExecuteScalarAsync<double>();
3939
}
4040

4141
[Benchmark]
42-
public async Task<decimal> SqlKata_Sum()
42+
public async Task<double> SqlKata_Sum()
4343
{
4444
var query = new Query("orders").AsSum("Total");
4545
var compiled = SqlKataCompiler.Compile(query);
@@ -51,6 +51,6 @@ public async Task<decimal> SqlKata_Sum()
5151
cmd.Parameters.AddWithValue($"@p{cmd.Parameters.Count}", binding);
5252
}
5353
var result = await cmd.ExecuteScalarAsync();
54-
return Convert.ToDecimal(result);
54+
return Convert.ToDouble(result);
5555
}
5656
}

src/Quarry.Benchmarks/Benchmarks/ComplexJoinFilterPaginateBenchmarks.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class ComplexJoinFilterPaginateBenchmarks : BenchmarkBase
1513
{
1614
[Benchmark(Baseline = true)]
@@ -31,7 +29,7 @@ LIMIT 10 OFFSET 5
3129
results.Add(new UserOrderDto
3230
{
3331
UserName = reader.GetString(0),
34-
Total = reader.GetDecimal(1)
32+
Total = reader.GetDouble(1)
3533
});
3634
}
3735
return results;
@@ -105,7 +103,7 @@ public async Task<List<UserOrderDto>> SqlKata_JoinFilterPaginate()
105103
results.Add(new UserOrderDto
106104
{
107105
UserName = reader.GetString(0),
108-
Total = reader.GetDecimal(1)
106+
Total = reader.GetDouble(1)
109107
});
110108
}
111109
return results;

src/Quarry.Benchmarks/Benchmarks/CteMultiBenchmarks.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class CteMultiBenchmarks : BenchmarkBase
1513
{
1614
private const string MultiCteSql = """
@@ -37,7 +35,7 @@ public async Task<List<OrderIdTotalDto>> Raw_MultiCte()
3735
results.Add(new OrderIdTotalDto
3836
{
3937
OrderId = reader.GetInt32(0),
40-
Total = reader.GetDecimal(1)
38+
Total = reader.GetDouble(1)
4139
});
4240
}
4341
return results;
@@ -84,7 +82,7 @@ public async Task<List<OrderIdTotalDto>> SqlKata_MultiCte_RawFallback()
8482
results.Add(new OrderIdTotalDto
8583
{
8684
OrderId = reader.GetInt32(0),
87-
Total = reader.GetDecimal(1)
85+
Total = reader.GetDouble(1)
8886
});
8987
}
9088
return results;

src/Quarry.Benchmarks/Benchmarks/CteProjectionBenchmarks.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class CteProjectionBenchmarks : BenchmarkBase
1513
{
1614
private const string CteProjectionSql = """
@@ -33,7 +31,7 @@ public async Task<List<OrderIdTotalDto>> Raw_CteProjection()
3331
results.Add(new OrderIdTotalDto
3432
{
3533
OrderId = reader.GetInt32(0),
36-
Total = reader.GetDecimal(1)
34+
Total = reader.GetDouble(1)
3735
});
3836
}
3937
return results;
@@ -86,7 +84,7 @@ public async Task<List<OrderIdTotalDto>> SqlKata_CteProjection_RawFallback()
8684
results.Add(new OrderIdTotalDto
8785
{
8886
OrderId = reader.GetInt32(0),
89-
Total = reader.GetDecimal(1)
87+
Total = reader.GetDouble(1)
9088
});
9189
}
9290
return results;

src/Quarry.Benchmarks/Benchmarks/CteSimpleBenchmarks.cs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// NOTE — Reader floor for decimal columns on SQLite (canonical explanation; other
13-
// decimal-reading benchmarks reference this file):
14-
//
15-
// Microsoft.Data.Sqlite.GetDecimal(int) is implemented as:
16-
// decimal.Parse(GetString(ordinal), NumberStyles.Number | AllowExponent, InvariantCulture)
17-
// (see SqliteValueReader.cs in dotnet/efcore). That's a string allocation +
18-
// culture-aware parse per cell — ~12 µs of the per-query floor in these benchmarks.
19-
// Quarry's generated reader emits `r.GetDecimal(N)` and hits the same path, so
20-
// Quarry tracks the hand-rolled Raw baseline within noise (~0.2%).
21-
//
22-
// Dapper's IL-emitted deserializer reads every column through the DbDataReader
23-
// indexer (`get_Item(int)`), which delegates to GetValue → SQLite REAL column
24-
// returns a boxed double → IL unbox + `(decimal)(double)box` conversion.
25-
// Verified empirically by wrapping the reader in a logging proxy; for a
26-
// (int, decimal) projection Dapper called only `get_Item(int)`, never
27-
// GetDecimal/GetValue/GetFieldValue<T>/GetString. That skips the string-parse
28-
// path (faster) but boxes per cell (more allocation, and quietly loses precision
29-
// past ~15 significant digits). The benchmark numbers reflect that trade.
30-
//
31-
// Quarry intentionally does NOT do the (decimal)GetDouble(ordinal) trick: silent
32-
// precision loss on Col<decimal> would corrupt currency/financial round-trips.
33-
// The floor here is a Microsoft.Data.Sqlite driver characteristic, not a library
34-
// overhead.
3512
public class CteSimpleBenchmarks : BenchmarkBase
3613
{
3714
private const string SimpleCteFilterSql = """
@@ -54,7 +31,7 @@ public async Task<List<OrderIdTotalDto>> Raw_SimpleCte()
5431
results.Add(new OrderIdTotalDto
5532
{
5633
OrderId = reader.GetInt32(0),
57-
Total = reader.GetDecimal(1)
34+
Total = reader.GetDouble(1)
5835
});
5936
}
6037
return results;
@@ -101,7 +78,7 @@ public async Task<List<OrderIdTotalDto>> SqlKata_SimpleCte_RawFallback()
10178
results.Add(new OrderIdTotalDto
10279
{
10380
OrderId = reader.GetInt32(0),
104-
Total = reader.GetDecimal(1)
81+
Total = reader.GetDouble(1)
10582
});
10683
}
10784
return results;

src/Quarry.Benchmarks/Benchmarks/JoinInnerBenchmarks.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class JoinInnerBenchmarks : BenchmarkBase
1513
{
1614
[Benchmark(Baseline = true)]
@@ -25,7 +23,7 @@ public async Task<List<UserOrderDto>> Raw_InnerJoin()
2523
results.Add(new UserOrderDto
2624
{
2725
UserName = reader.GetString(0),
28-
Total = reader.GetDecimal(1)
26+
Total = reader.GetDouble(1)
2927
});
3028
}
3129
return results;
@@ -85,7 +83,7 @@ public async Task<List<UserOrderDto>> SqlKata_InnerJoin()
8583
results.Add(new UserOrderDto
8684
{
8785
UserName = reader.GetString(0),
88-
Total = reader.GetDecimal(1)
86+
Total = reader.GetDouble(1)
8987
});
9088
}
9189
return results;

src/Quarry.Benchmarks/Benchmarks/JoinThreeTableBenchmarks.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class JoinThreeTableBenchmarks : BenchmarkBase
1513
{
1614
[Benchmark(Baseline = true)]
@@ -30,7 +28,7 @@ FROM users u
3028
results.Add(new UserOrderItemDto
3129
{
3230
UserName = reader.GetString(0),
33-
Total = reader.GetDecimal(1),
31+
Total = reader.GetDouble(1),
3432
ProductName = reader.GetString(2)
3533
});
3634
}
@@ -100,7 +98,7 @@ public async Task<List<UserOrderItemDto>> SqlKata_ThreeTableJoin()
10098
results.Add(new UserOrderItemDto
10199
{
102100
UserName = reader.GetString(0),
103-
Total = reader.GetDecimal(1),
101+
Total = reader.GetDouble(1),
104102
ProductName = reader.GetString(2)
105103
});
106104
}

src/Quarry.Benchmarks/Benchmarks/WindowLagBenchmarks.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class WindowLagBenchmarks : BenchmarkBase
1513
{
1614
// ── LAG(Total, 1) OVER (ORDER BY OrderDate) ──
@@ -30,17 +28,17 @@ public async Task<List<OrderLagDto>> Raw_Lag()
3028
results.Add(new OrderLagDto
3129
{
3230
OrderId = reader.GetInt32(0),
33-
Total = reader.GetDecimal(1),
34-
PrevTotal = reader.IsDBNull(2) ? null : reader.GetDecimal(2)
31+
Total = reader.GetDouble(1),
32+
PrevTotal = reader.IsDBNull(2) ? null : reader.GetDouble(2)
3533
});
3634
}
3735
return results;
3836
}
3937

4038
[Benchmark]
41-
public async Task<List<DapperOrderLagDto>> Dapper_Lag()
39+
public async Task<List<OrderLagDto>> Dapper_Lag()
4240
{
43-
return (await Connection.QueryAsync<DapperOrderLagDto>(LagSql)).AsList();
41+
return (await Connection.QueryAsync<OrderLagDto>(LagSql)).AsList();
4442
}
4543

4644
[Benchmark]
@@ -80,8 +78,8 @@ public async Task<List<OrderLagDto>> SqlKata_Lag_RawFallback()
8078
results.Add(new OrderLagDto
8179
{
8280
OrderId = reader.GetInt32(0),
83-
Total = reader.GetDecimal(1),
84-
PrevTotal = reader.IsDBNull(2) ? null : reader.GetDecimal(2)
81+
Total = reader.GetDouble(1),
82+
PrevTotal = reader.IsDBNull(2) ? null : reader.GetDouble(2)
8583
});
8684
}
8785
return results;

src/Quarry.Benchmarks/Benchmarks/WindowRunningSumBenchmarks.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace Quarry.Benchmarks.Benchmarks;
1111

12-
// Reader floor here is bounded by Microsoft.Data.Sqlite.GetDecimal (string-parse path).
13-
// See CteSimpleBenchmarks.cs for the full explanation of why Dapper appears faster.
1412
public class WindowRunningSumBenchmarks : BenchmarkBase
1513
{
1614
// ── SUM(Total) OVER (PARTITION BY Status) — Running Sum ──
@@ -30,8 +28,8 @@ public async Task<List<OrderRunningSumDto>> Raw_RunningSum()
3028
results.Add(new OrderRunningSumDto
3129
{
3230
OrderId = reader.GetInt32(0),
33-
Total = reader.GetDecimal(1),
34-
RunningSum = reader.GetDecimal(2)
31+
Total = reader.GetDouble(1),
32+
RunningSum = reader.GetDouble(2)
3533
});
3634
}
3735
return results;
@@ -80,8 +78,8 @@ public async Task<List<OrderRunningSumDto>> SqlKata_RunningSum_RawFallback()
8078
results.Add(new OrderRunningSumDto
8179
{
8280
OrderId = reader.GetInt32(0),
83-
Total = reader.GetDecimal(1),
84-
RunningSum = reader.GetDecimal(2)
81+
Total = reader.GetDouble(1),
82+
RunningSum = reader.GetDouble(2)
8583
});
8684
}
8785
return results;

0 commit comments

Comments
 (0)