@@ -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 : '(' ) ;
0 commit comments