Skip to content

Commit 7324329

Browse files
committed
Multiple Sort (ThenBy implementation)
287 Tests all passed.
1 parent 75b9c2f commit 7324329

18 files changed

Lines changed: 383 additions & 63 deletions

File tree

LiteDBX.Tests/Engine/Collation_Tests.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ public async Task Culture_Ordinal_Sort()
6161
await e.Insert("col1", names.Select(x => new BsonDocument { ["name"] = x }), BsonAutoId.Int32);
6262

6363
// sort by merge sort
64-
var sortByOrderByName = await e.Query("col1", new Query { OrderBy = "name" })
64+
var orderQuery = new Query();
65+
orderQuery.OrderBy.Add(new QueryOrder("name", Query.Ascending));
66+
67+
var sortByOrderByName = await e.Query("col1", orderQuery)
6568
.Select(x => x["name"].AsString)
6669
.ToListAsync();
6770

@@ -78,7 +81,10 @@ public async Task Culture_Ordinal_Sort()
7881
// index test
7982
await e.EnsureIndex("col1", "idx_name", "name", false);
8083

81-
var sortByIndexName = await e.Query("col1", new Query { OrderBy = "name" })
84+
var indexOrderQuery = new Query();
85+
indexOrderQuery.OrderBy.Add(new QueryOrder("name", Query.Ascending));
86+
87+
var sortByIndexName = await e.Query("col1", indexOrderQuery)
8288
.Select(x => x["name"].AsString)
8389
.ToListAsync();
8490

LiteDBX.Tests/Internals/Sort_Tests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void Sort_String_Asc()
2525

2626
using (var tempDisk = new SortDisk(_factory, 10 * 8192, pragmas))
2727
{
28-
using (var s = new SortService(tempDisk, Query.Ascending, pragmas))
28+
using (var s = new SortService(tempDisk, new[] { Query.Ascending }, pragmas))
2929
{
3030
s.Insert(source);
3131

@@ -55,7 +55,7 @@ public void Sort_Int_Desc()
5555

5656
using (var tempDisk = new SortDisk(_factory, 10 * 8192, pragmas))
5757
{
58-
using (var s = new SortService(tempDisk, Query.Descending, pragmas))
58+
using (var s = new SortService(tempDisk, new[] { Query.Descending }, pragmas))
5959
{
6060
s.Insert(source);
6161

LiteDBX.Tests/Query/OrderBy_Tests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,78 @@ public async Task Query_Asc_Desc()
7777
asc[0].Id.Should().Be(1);
7878
desc[0].Id.Should().Be(1000);
7979
}
80+
81+
[Fact]
82+
public async Task Query_OrderBy_ThenBy_Multiple_Keys()
83+
{
84+
await using var db = await PersonQueryData.CreateAsync();
85+
var (collection, local) = db.GetData();
86+
87+
await collection.EnsureIndex(x => x.Age);
88+
89+
var expected = local
90+
.OrderBy(x => x.Age)
91+
.ThenByDescending(x => x.Name)
92+
.Select(x => new { x.Age, x.Name })
93+
.ToArray();
94+
95+
var actual = await collection.Query()
96+
.OrderBy(x => x.Age)
97+
.ThenByDescending(x => x.Name)
98+
.Select(x => new { x.Age, x.Name })
99+
.ToArray();
100+
101+
actual.Should().Equal(expected);
102+
103+
var plan = await collection.Query()
104+
.OrderBy(x => x.Age)
105+
.ThenByDescending(x => x.Name)
106+
.GetPlan();
107+
108+
plan["index"]["order"].AsInt32.Should().Be(Query.Ascending);
109+
110+
var orderBy = plan["orderBy"].AsArray;
111+
112+
orderBy.Count.Should().Be(2);
113+
orderBy[0]["expr"].AsString.Should().Be("$.Age");
114+
orderBy[0]["order"].AsInt32.Should().Be(Query.Ascending);
115+
orderBy[1]["expr"].AsString.Should().Be("$.Name");
116+
orderBy[1]["order"].AsInt32.Should().Be(Query.Descending);
117+
}
118+
119+
[Fact]
120+
public async Task Query_OrderByDescending_ThenBy_Index_Order_Applied()
121+
{
122+
await using var db = await PersonQueryData.CreateAsync();
123+
var (collection, local) = db.GetData();
124+
125+
await collection.EnsureIndex(x => x.Name);
126+
127+
var expected = local
128+
.OrderByDescending(x => x.Name)
129+
.ThenBy(x => x.Age)
130+
.Select(x => new { x.Name, x.Age })
131+
.ToArray();
132+
133+
var actual = await collection.Query()
134+
.OrderByDescending(x => x.Name)
135+
.ThenBy(x => x.Age)
136+
.Select(x => new { x.Name, x.Age })
137+
.ToArray();
138+
139+
actual.Should().Equal(expected);
140+
141+
var plan = await collection.Query()
142+
.OrderByDescending(x => x.Name)
143+
.ThenBy(x => x.Age)
144+
.GetPlan();
145+
146+
plan["index"]["order"].AsInt32.Should().Be(Query.Descending);
147+
148+
var orderBy = plan["orderBy"].AsArray;
149+
150+
orderBy.Count.Should().Be(2);
151+
orderBy[0]["order"].AsInt32.Should().Be(Query.Descending);
152+
orderBy[1]["order"].AsInt32.Should().Be(Query.Ascending);
153+
}
80154
}

LiteDBX/Client/Database/ILiteQueryable.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public interface ILiteQueryable<T> : ILiteQueryableResult<T>
3131
ILiteQueryable<T> OrderBy<K>(Expression<Func<T, K>> keySelector, int order = 1);
3232
ILiteQueryable<T> OrderByDescending(BsonExpression keySelector);
3333
ILiteQueryable<T> OrderByDescending<K>(Expression<Func<T, K>> keySelector);
34+
ILiteQueryable<T> ThenBy(BsonExpression keySelector);
35+
ILiteQueryable<T> ThenBy<K>(Expression<Func<T, K>> keySelector);
36+
ILiteQueryable<T> ThenByDescending(BsonExpression keySelector);
37+
ILiteQueryable<T> ThenByDescending<K>(Expression<Func<T, K>> keySelector);
3438

3539
// ── Group ─────────────────────────────────────────────────────────────────
3640
ILiteQueryable<T> GroupBy(BsonExpression keySelector);

LiteDBX/Client/Database/LiteQueryable.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ public ILiteQueryable<T> Where(Expression<Func<T, bool>> predicate)
112112

113113
public ILiteQueryable<T> OrderBy(BsonExpression keySelector, int order = Query.Ascending)
114114
{
115-
if (_query.OrderBy != null) throw new ArgumentException("ORDER BY already defined in this query builder");
116-
_query.OrderBy = keySelector;
117-
_query.Order = order;
115+
if (_query.OrderBy.Count > 0) throw new ArgumentException("ORDER BY already defined in this query builder");
116+
_query.OrderBy.Add(new QueryOrder(keySelector, order));
118117
return this;
119118
}
120119

@@ -127,6 +126,32 @@ public ILiteQueryable<T> OrderBy<K>(Expression<Func<T, K>> keySelector, int orde
127126

128127
public ILiteQueryable<T> OrderByDescending<K>(Expression<Func<T, K>> keySelector) => OrderBy(keySelector, Query.Descending);
129128

129+
public ILiteQueryable<T> ThenBy(BsonExpression keySelector)
130+
{
131+
if (_query.OrderBy.Count == 0) return OrderBy(keySelector, Query.Ascending);
132+
133+
_query.OrderBy.Add(new QueryOrder(keySelector, Query.Ascending));
134+
return this;
135+
}
136+
137+
public ILiteQueryable<T> ThenBy<K>(Expression<Func<T, K>> keySelector)
138+
{
139+
return ThenBy(_mapper.GetExpression(keySelector));
140+
}
141+
142+
public ILiteQueryable<T> ThenByDescending(BsonExpression keySelector)
143+
{
144+
if (_query.OrderBy.Count == 0) return OrderBy(keySelector, Query.Descending);
145+
146+
_query.OrderBy.Add(new QueryOrder(keySelector, Query.Descending));
147+
return this;
148+
}
149+
150+
public ILiteQueryable<T> ThenByDescending<K>(Expression<Func<T, K>> keySelector)
151+
{
152+
return ThenByDescending(_mapper.GetExpression(keySelector));
153+
}
154+
130155
#endregion
131156

132157
#region Select

LiteDBX/Client/SqlParser/Commands/Select.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,31 @@ private IBsonDataReader ParseSelect(CancellationToken cancellationToken = defaul
8888
{
8989
_tokenizer.ReadToken();
9090
_tokenizer.ReadToken().Expect("BY");
91-
query.OrderBy = BsonExpression.Create(_tokenizer, BsonExpressionParserMode.Full, _parameters);
9291

93-
var orderByOrder = Query.Ascending;
94-
var orderByToken = _tokenizer.LookAhead();
95-
96-
if (orderByToken.Is("ASC") || orderByToken.Is("DESC"))
92+
while (true)
9793
{
98-
orderByOrder = _tokenizer.ReadToken().Is("ASC") ? Query.Ascending : Query.Descending;
99-
}
94+
var orderBy = BsonExpression.Create(_tokenizer, BsonExpressionParserMode.Full, _parameters);
95+
96+
var orderByOrder = Query.Ascending;
97+
var orderByToken = _tokenizer.LookAhead();
98+
99+
if (orderByToken.Is("ASC") || orderByToken.Is("DESC"))
100+
{
101+
orderByOrder = _tokenizer.ReadToken().Is("ASC") ? Query.Ascending : Query.Descending;
102+
}
100103

101-
query.Order = orderByOrder;
104+
query.OrderBy.Add(new QueryOrder(orderBy, orderByOrder));
105+
106+
var next = _tokenizer.LookAhead();
107+
108+
if (next.Type == TokenType.Comma)
109+
{
110+
_tokenizer.ReadToken();
111+
continue;
112+
}
113+
114+
break;
115+
}
102116
}
103117

104118
ahead = _tokenizer.LookAhead().Expect(TokenType.Word, TokenType.EOF, TokenType.SemiColon);

LiteDBX/Client/Structures/Query.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ public static Query All()
3333
/// </summary>
3434
public static Query All(int order = Ascending)
3535
{
36-
return new Query { OrderBy = "_id", Order = order };
36+
var query = new Query();
37+
query.OrderBy.Add(new QueryOrder(BsonExpression.Create("_id"), order));
38+
return query;
3739
}
3840

3941
/// <summary>
4042
/// Returns all documents
4143
/// </summary>
4244
public static Query All(string field, int order = Ascending)
4345
{
44-
return new Query { OrderBy = field, Order = order };
46+
var query = new Query();
47+
query.OrderBy.Add(new QueryOrder(BsonExpression.Create(field), order));
48+
return query;
4549
}
4650

4751
/// <summary>

LiteDBX/Engine/Query/Pipeline/BasePipe.cs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -164,24 +164,63 @@ protected IEnumerable<BsonDocument> Filter(IEnumerable<BsonDocument> source, Bso
164164
/// <summary>
165165
/// ORDER BY: Sort documents according orderby expression and order asc/desc
166166
/// </summary>
167-
protected IEnumerable<BsonDocument> OrderBy(IEnumerable<BsonDocument> source, BsonExpression expr, int order, int offset, int limit)
167+
protected IEnumerable<BsonDocument> OrderBy(IEnumerable<BsonDocument> source, OrderBy orderBy, int offset, int limit)
168168
{
169-
var keyValues = source
170-
.Select(x => new KeyValuePair<BsonValue, PageAddress>(expr.ExecuteScalar(x, _pragmas.Collation), x.RawId));
169+
var segments = orderBy.Segments;
171170

172-
using (var sorter = new SortService(_tempDisk, order, _pragmas))
171+
if (segments.Count == 1)
173172
{
174-
sorter.Insert(keyValues);
173+
var segment = segments[0];
174+
var keyValues = source
175+
.Select(doc => new KeyValuePair<BsonValue, PageAddress>(segment.Expression.ExecuteScalar(doc, _pragmas.Collation), doc.RawId));
175176

176-
LOG($"sort {sorter.Count} keys in {sorter.Containers.Count} containers", "SORT");
177+
using (var sorter = new SortService(_tempDisk, new[] { segment.Order }, _pragmas))
178+
{
179+
sorter.Insert(keyValues);
180+
181+
LOG($"sort {sorter.Count} keys in {sorter.Containers.Count} containers", "SORT");
182+
183+
var result = sorter.Sort().Skip(offset).Take(limit);
177184

178-
var result = sorter.Sort().Skip(offset).Take(limit);
185+
foreach (var keyValue in result)
186+
{
187+
var doc = _lookup.Load(keyValue.Value);
188+
189+
yield return doc;
190+
}
191+
}
192+
}
193+
else
194+
{
195+
var orders = segments.Select(x => x.Order).ToArray();
196+
197+
var keyValues = source
198+
.Select(doc =>
199+
{
200+
var values = new BsonValue[segments.Count];
179201

180-
foreach (var keyValue in result)
202+
for (var i = 0; i < segments.Count; i++)
203+
{
204+
values[i] = segments[i].Expression.ExecuteScalar(doc, _pragmas.Collation);
205+
}
206+
207+
return new KeyValuePair<BsonValue, PageAddress>(SortKey.FromValues(values, orders), doc.RawId);
208+
});
209+
210+
using (var sorter = new SortService(_tempDisk, orders, _pragmas))
181211
{
182-
var doc = _lookup.Load(keyValue.Value);
212+
sorter.Insert(keyValues);
183213

184-
yield return doc;
214+
LOG($"sort {sorter.Count} keys in {sorter.Containers.Count} containers", "SORT");
215+
216+
var result = sorter.Sort().Skip(offset).Take(limit);
217+
218+
foreach (var keyValue in result)
219+
{
220+
var doc = _lookup.Load(keyValue.Value);
221+
222+
yield return doc;
223+
}
185224
}
186225
}
187226
}

LiteDBX/Engine/Query/Pipeline/GroupByPipe.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public override IEnumerable<BsonDocument> Pipe(IEnumerable<IndexNode> nodes, Que
4141
// run orderBy used in GroupBy (if not already ordered by index)
4242
if (query.OrderBy != null)
4343
{
44-
source = OrderBy(source, query.OrderBy.Expression, query.OrderBy.Order, 0, int.MaxValue);
44+
source = OrderBy(source, query.OrderBy, 0, int.MaxValue);
4545
}
4646

4747
// apply groupby

LiteDBX/Engine/Query/Pipeline/QueryPipe.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public override IEnumerable<BsonDocument> Pipe(IEnumerable<IndexNode> nodes, Que
4848
if (query.OrderBy != null)
4949
{
5050
// pipe: orderby with offset+limit
51-
source = OrderBy(source, query.OrderBy.Expression, query.OrderBy.Order, query.Offset, query.Limit);
51+
source = OrderBy(source, query.OrderBy, query.Offset, query.Limit);
5252
}
5353
else
5454
{

0 commit comments

Comments
 (0)