Skip to content

Commit c90d3b8

Browse files
Merge pull request #7 from fadhly-permata/dev
Dev
2 parents 3104c67 + 99aab03 commit c90d3b8

File tree

5 files changed

+241
-50
lines changed

5 files changed

+241
-50
lines changed

Core/JsonQueryEngine.cs

Lines changed: 166 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ public static IEnumerable<JToken> Execute(JsonQueryRequest request)
4545
query = PerformGrouping(request: request, query: query);
4646
query = PerformSorting(request: request, query: query);
4747

48-
return query;
48+
var result = query;
49+
50+
// Apply DISTINCT if requested
51+
if (request.Distinct)
52+
result = result.Distinct(new JTokenEqualityComparer());
53+
54+
return result;
4955
}
5056
catch (Exception ex)
5157
{
@@ -139,16 +145,22 @@ IEnumerable<JToken> query
139145

140146
private static JToken? GetSourceToken(JObject root, string path)
141147
{
148+
// Jika path tidak dimulai dengan $, tambahkan $. secara otomatis
149+
if (!path.StartsWith('$') && !path.StartsWith('['))
150+
{
151+
path = "$." + path;
152+
}
153+
142154
if (path == "$")
143155
return root;
144156

145-
if (path.StartsWith(value: "$."))
157+
if (path.StartsWith("$."))
146158
{
147-
var cleanPath = path[2..];
148-
return root.SelectToken(path: cleanPath) ?? root[cleanPath];
159+
string cleanPath = path[2..];
160+
return root.SelectToken(cleanPath) ?? root[cleanPath];
149161
}
150162

151-
return root.SelectToken(path: path) ?? root[path];
163+
return root.SelectToken(path) ?? root[path];
152164
}
153165

154166
private static IEnumerable<JToken> ApplyJoin(
@@ -218,20 +230,78 @@ string joinStatement
218230
});
219231
}
220232

221-
private static JObject ProjectItem(JObject root, JToken item, string[] select)
233+
private static JToken ProjectItem(JObject root, JToken item, string[] select)
222234
{
235+
// Handle khusus untuk SELECT $.*
236+
if (select.Length == 1 && select[0] == "$.*")
237+
{
238+
return item.DeepClone();
239+
}
240+
241+
// Handle khusus untuk SELECT * atau alias.*
242+
if (select.Length == 1 && select[0].EndsWith(".*"))
243+
{
244+
string alias = select[0].Split('.')[0];
245+
if (item[alias] != null)
246+
{
247+
return item[alias].DeepClone();
248+
}
249+
return item.DeepClone();
250+
}
251+
252+
// Handle khusus untuk SELECT * (tanpa alias)
253+
if (select.Length == 1 && select[0] == "*")
254+
{
255+
if (item is JObject jObj)
256+
{
257+
var result = new JObject();
258+
foreach (var prop in jObj.Properties())
259+
{
260+
// Skip properti yang merupakan alias join
261+
if (!prop.Name.StartsWith("$"))
262+
{
263+
result[prop.Name] = prop.Value.DeepClone();
264+
}
265+
}
266+
return result;
267+
}
268+
return item;
269+
}
270+
271+
// Proyeksi normal untuk kolom spesifik
223272
JObject projectedObj = [];
224273
foreach (var selection in select)
225274
{
226-
var parts = selection.Split(
227-
separator: [" AS ", " as "],
228-
options: StringSplitOptions.RemoveEmptyEntries
229-
);
275+
var parts = selection.Split([" AS ", " as "], StringSplitOptions.RemoveEmptyEntries);
230276
string sourceField = parts[0].Trim();
277+
string aliasField =
278+
parts.Length > 1
279+
? parts[1].Trim()
280+
: (
281+
sourceField.Contains('.')
282+
? sourceField.Split('.').Last()
283+
: sourceField.Replace("$.", "")
284+
);
285+
286+
// Handle field dengan alias
287+
JToken? value = null;
288+
if (sourceField.Contains('.'))
289+
{
290+
var fieldParts = sourceField.Split('.');
291+
string alias = fieldParts[0];
292+
string field = fieldParts[1];
231293

232-
string aliasField = GetAliasField(parts: parts, sourceField: sourceField);
294+
if (item[alias] is JObject aliasObj)
295+
{
296+
value = aliasObj[field] ?? aliasObj.SelectToken(field);
297+
}
298+
}
299+
else
300+
{
301+
value = GetTokenValue(root, item, sourceField);
302+
}
233303

234-
projectedObj[aliasField] = GetTokenValue(root: root, item: item, path: sourceField);
304+
projectedObj[aliasField] = value?.DeepClone() ?? JValue.CreateNull();
235305
}
236306
return projectedObj;
237307
}
@@ -250,9 +320,24 @@ private static string GetAliasField(string[] parts, string sourceField)
250320
{
251321
if (path == "$")
252322
return root;
253-
if (path.StartsWith(value: "$."))
254-
return root.SelectToken(path: path[2..]);
255-
return item.SelectToken(path: path) ?? item[path];
323+
324+
if (path.StartsWith("$."))
325+
return root.SelectToken(path[2..]);
326+
327+
// Handle path dengan alias (misal: t.CustomerName)
328+
if (path.Contains('.'))
329+
{
330+
var parts = path.Split('.');
331+
string alias = parts[0];
332+
string field = string.Join(".", parts.Skip(1));
333+
334+
if (item[alias] is JToken aliasToken)
335+
{
336+
return aliasToken.SelectToken(field) ?? aliasToken[field];
337+
}
338+
}
339+
340+
return item.SelectToken(path) ?? item[path];
256341
}
257342

258343
private static bool EvaluateConditions(JObject root, JToken item, string[] conditions)
@@ -410,21 +495,79 @@ JObject resultObj
410495
private static IEnumerable<JToken> ApplyOrdering(IEnumerable<JToken> query, string[] order)
411496
{
412497
IOrderedEnumerable<JToken>? orderedQuery = null;
498+
413499
for (int i = 0; i < order.Length; i++)
414500
{
415-
string column = order[i];
416-
if (i == 0)
417-
orderedQuery = query.OrderBy(keySelector: item =>
418-
item.SelectToken(path: column) ?? item[column]
419-
);
501+
var clause = order[i].Trim();
502+
var parts = clause.Split(' ', StringSplitOptions.RemoveEmptyEntries);
503+
if (parts.Length == 0)
504+
continue;
505+
506+
string column = parts[0];
507+
bool isDescending =
508+
parts.Length > 1 && parts[1].Equals("DESC", StringComparison.OrdinalIgnoreCase);
509+
510+
if (orderedQuery == null)
511+
{
512+
orderedQuery = isDescending
513+
? query.OrderByDescending(item => GetOrderValue(item, column))
514+
: query.OrderBy(item => GetOrderValue(item, column));
515+
}
420516
else
421-
orderedQuery = orderedQuery!.ThenBy(keySelector: item =>
422-
item.SelectToken(path: column) ?? item[column]
423-
);
517+
{
518+
orderedQuery = isDescending
519+
? orderedQuery.ThenByDescending(item => GetOrderValue(item, column))
520+
: orderedQuery.ThenBy(item => GetOrderValue(item, column));
521+
}
424522
}
523+
425524
return orderedQuery ?? query;
426525
}
427526

527+
private static object? GetOrderValue(JToken item, string path)
528+
{
529+
try
530+
{
531+
// Cari token berdasarkan path
532+
var token = item.SelectToken(path) ?? item[path];
533+
534+
// Jika tidak ditemukan dan path mengandung titik (untuk alias)
535+
if (token == null && path.Contains('.'))
536+
{
537+
var pathParts = path.Split('.');
538+
if (pathParts.Length == 2)
539+
{
540+
var alias = pathParts[0];
541+
var field = pathParts[1];
542+
543+
if (item[alias] is JObject aliasObj)
544+
{
545+
token = aliasObj[field] ?? aliasObj.SelectToken(field);
546+
}
547+
}
548+
}
549+
550+
if (token == null)
551+
return null;
552+
553+
// Kembalikan nilai berdasarkan tipe token
554+
return token.Type switch
555+
{
556+
JTokenType.Integer => token.Value<long>(),
557+
JTokenType.Float => token.Value<double>(),
558+
JTokenType.String => token.Value<string>() ?? string.Empty,
559+
JTokenType.Boolean => token.Value<bool>(),
560+
JTokenType.Date => token.Value<DateTime>(),
561+
JTokenType.Null => null,
562+
_ => token.ToString(),
563+
};
564+
}
565+
catch
566+
{
567+
return null;
568+
}
569+
}
570+
428571
private static JToken CalculateAggregate(IEnumerable<JToken> group, string expression)
429572
{
430573
var openParen = expression.IndexOf(value: '(');

Extensions/JsonQueryExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
namespace JQL.Net.Extensions;
55

6+
/// <summary>
7+
/// Extension methods for JObject
8+
/// </summary>
69
public static class JsonQueryExtensions
710
{
811
/// <summary>

GlobalSuppressions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[assembly: SuppressMessage(
44
"SonarLint",
55
"S1192",
6-
Justification = "SQL keywords are standard and unlikely to change",
7-
Scope = "namespace",
8-
Target = "~N:JQL.Net.Core"
6+
Justification = "SQL keywords in Parse method are standard and need to be literals for clarity",
7+
Scope = "member",
8+
Target = "~M:JQL.Net.JsonQueryRequest.Parse(System.String)"
99
)]

JQL.Net.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<TargetFramework>net8.0</TargetFramework>
44
<PackageId>JQL.Net</PackageId>
5-
<Version>1.0.4</Version>
5+
<Version>1.0.5</Version>
66
<Authors>Fadhly Permata</Authors>
77
<Company>Fadhly Permata</Company>
88
<Description>JQL.Net: A lightweight SQL-inspired engine to query, join, and aggregate JSON in .NET. No database? No problem!</Description>

0 commit comments

Comments
 (0)