@@ -300,106 +300,99 @@ def json_KeyTransformExact_process_rhs(self, compiler, connection):
300300 rhs_params = unquote_json_rhs (rhs_params )
301301 return rhs , rhs_params
302302
303-
304303def json_KeyTransformIn (self , compiler , connection ):
305304 lhs , _ = super (KeyTransformIn , self ).process_lhs (compiler , connection )
306305 rhs , rhs_params = super (KeyTransformIn , self ).process_rhs (compiler , connection )
307306
308307 return (lhs + ' IN ' + rhs , unquote_json_rhs (rhs_params ))
309308
310- # This handles the case where the JSON data comes from a table column (actual database data).
311- # Also deals with hardcoded JSON string literal seperately, since handling differs for literals vs. table data
312309def json_HasKeyLookup (self , compiler , connection ):
313310 """
314311 Implementation of HasKey lookup for SQL Server.
315- Handles for both SQL Server 2022+ (using JSON_PATH_EXISTS) and older versions (using OPENJSON).
312+
313+ Supports two methods depending on SQL Server version:
314+ - SQL Server 2022+: Uses JSON_PATH_EXISTS function
315+ - Older versions: Uses JSON_VALUE IS NOT NULL
316316 """
317- # Determine the JSON path for the left-hand side (lhs).
318- # If dealing with a nested JSON structure, use KeyTransform to extract the path.
317+
318+ def _combine_conditions (conditions ):
319+ # Combine multiple conditions using the logical operator if present, otherwise return the first condition
320+ if hasattr (self , 'logical_operator' ) and self .logical_operator :
321+ logical_op = f" { self .logical_operator } "
322+ return f"({ logical_op .join (conditions )} )"
323+ else :
324+ return conditions [0 ]
325+
326+ # Process JSON path from the left-hand side.
319327 if isinstance (self .lhs , KeyTransform ):
328+ # If lhs is a KeyTransform, preprocess to get SQL and JSON path
320329 lhs , _ , lhs_key_transforms = self .lhs .preprocess_lhs (compiler , connection )
321330 lhs_json_path = compile_json_path (lhs_key_transforms )
322- # if dealing with the JSON and not with nested structure then else block will be executed
331+ lhs_params = []
323332 else :
333+ # Otherwise, process lhs normally and set default JSON path
324334 lhs , lhs_params = self .process_lhs (compiler , connection )
325- lhs_json_path = "$"
335+ lhs_json_path = '$'
326336
327337 # Check if we're dealing with a Cast expression (literal JSON value)
328338 is_cast_expression = isinstance (self .lhs , Cast )
329339
330340 # Process JSON paths from the right-hand side
331341 rhs = self .rhs
332- # rhs_params stored the complete JSON path
333- rhs_params = []
334- # Convert single values into a list for uniform processing
335- # If rhs is not already a list or tuple (i.e., it's a single key),
336- # wrap it in a list so we can handle both single and multiple keys
337342 if not isinstance (rhs , (list , tuple )):
343+ # Ensure rhs is a list for uniform processing
338344 rhs = [rhs ]
345+
346+ rhs_params = []
339347 for key in rhs :
340- # if dealing with the nested JSON structure then if block will be executed
341348 if isinstance (key , KeyTransform ):
349+ # If key is a KeyTransform, preprocess to get transforms
342350 * _ , rhs_key_transforms = key .preprocess_lhs (compiler , connection )
343351 else :
352+ # Otherwise, treat key as a single transform
344353 rhs_key_transforms = [key ]
345- # Compile the full JSON path (lhs + rhs) according to the Django version in use
354+
346355 if VERSION >= (4 , 1 ):
356+ # For Django 4.1+, split out the final key and build the JSON path accordingly
347357 * rhs_key_transforms , final_key = rhs_key_transforms
348358 rhs_json_path = compile_json_path (rhs_key_transforms , include_root = False )
349359 rhs_json_path += self .compile_json_path_final_key (final_key )
350360 rhs_params .append (lhs_json_path + rhs_json_path )
351361 else :
362+ # For older Django, just compile the JSON path
352363 rhs_params .append (
353- "%s%s"
354- % (
364+ '%s%s' % (
355365 lhs_json_path ,
356- compile_json_path (rhs_key_transforms , include_root = False ),
366+ compile_json_path (rhs_key_transforms , include_root = False )
357367 )
358368 )
359369
360- # For SQL Server 2022+,use JSON_PATH_EXISTS
370+ # For SQL Server 2022+, use JSON_PATH_EXISTS
361371 if connection .sql_server_version >= 2022 :
372+ params = []
373+ conditions = []
362374 if is_cast_expression :
363- # For Cast expressions, manually construct SQL without %s placeholders
375+ # If lhs is a Cast, compile it to SQL and parameters
364376 cast_sql , cast_params = self .lhs .as_sql (compiler , connection )
365377
366- # Build conditions for each key
367- conditions = []
368378 for path in rhs_params :
369- # Escapes single quotes in the JSON path to avoid breaking SQL syntax.
379+ # Escape single quotes in the path for SQL
370380 path_escaped = path .replace ("'" , "''" )
371- # The > 0 checks that the path exists
372- conditions .append (
373- "JSON_PATH_EXISTS(" + cast_sql + ", '" + path_escaped + "') > 0"
374- )
375- # this if else block deals with forming this syntax (JSON_PATH_EXISTS(...) > 0 AND JSON_PATH_EXISTS(...) > 0)
376- if hasattr (self , "logical_operator" ) and self .logical_operator :
377- logical_op = " " + self .logical_operator + " "
378- sql = "(" + logical_op .join (conditions ) + ")"
379- else :
380- # if no operators are specified
381- sql = conditions [0 ]
381+ # Build the JSON_PATH_EXISTS condition
382+ conditions .append (f"JSON_PATH_EXISTS({ cast_sql } , '{ path_escaped } ') > 0" )
383+ params .extend (cast_params )
382384
383- return sql , cast_params
385+ return _combine_conditions ( conditions ), params
384386 else :
385- conditions = []
386387 for path in rhs_params :
387- # Escapes single quotes in the JSON path to avoid breaking SQL syntax.
388+ # Escape single quotes in the path for SQL
388389 path_escaped = path .replace ("'" , "''" )
389- conditions .append (
390- "JSON_PATH_EXISTS(" + lhs + ", '" + path_escaped + "') > 0"
391- )
390+ # Build the JSON_PATH_EXISTS condition using lhs
391+ conditions .append ("JSON_PATH_EXISTS(%s, '%s') > 0" % (lhs , path_escaped ))
392392
393- if hasattr (self , "logical_operator" ) and self .logical_operator :
394- logical_op = " " + self .logical_operator + " "
395- sql = "(" + logical_op .join (conditions ) + ")"
396- else :
397- sql = conditions [0 ]
393+ return _combine_conditions (conditions ), lhs_params
398394
399- # Return SQL with empty params list
400- return sql , []
401395 else :
402- # For older SQL Server versions
403396 if is_cast_expression :
404397 # SQL Server versions prior to 2022 do not support JSON_PATH_EXISTS,
405398 # and OPENJSON cannot be used on literal JSON values (i.e., values not stored in a table column).
@@ -409,41 +402,17 @@ def json_HasKeyLookup(self, compiler, connection):
409402 # we return a constant true condition ("1=1") with no parameters, which effectively returns all rows.
410403 return "1=1" , []
411404 else :
412- # Handling for versions prior to SQL Server 2022
413- # For older SQL Server versions, we use OPENJSON to check for the existence of keys in JSON data.
414- if VERSION >= (4 , 2 ):
415- try :
416- # Get table name from compiler query for Django 4.2+
417- # This retrieves the alias Django assigned to the main table in the SQL query.
418- # An alias is something like "T1" or "U0" that Django uses internally in the SQL it generates.
419- main_alias = compiler .query .get_initial_alias ()
420- # Get the table name from the alias map
421- table_name = compiler .query .alias_map [main_alias ].table_name
422- except (AttributeError , KeyError ):
423- # Fallback to traditional method
424- table_name = self .lhs .output_field .model ._meta .db_table
425- else :
426- table_name = self .lhs .output_field .model ._meta .db_table
427-
428- # Build SQL conditions with string concatenation
429405 conditions = []
430406 for path in rhs_params :
431- # Escapes single quotes in the JSON path to avoid breaking SQL syntax.
432- path_escaped = path .replace ("'" , "''" )
433- condition = (lhs + " IN (SELECT " + lhs + " FROM " + table_name +
434- " CROSS APPLY OPENJSON(" + lhs + ") WITH ([json_path_value] char(1) '" +
435- path_escaped + "') WHERE [json_path_value] IS NOT NULL)" )
436- conditions .append (condition )
437-
438- if hasattr (self , "logical_operator" ) and self .logical_operator :
439- logical_op = " " + self .logical_operator + " "
440- sql = "(" + logical_op .join (conditions ) + ")"
441- else :
442- sql = conditions [0 ]
407+ # Escape single quotes in the path for SQL
408+ path_escaped = path .replace ("'" , "''" )
409+ # Build the JSON_VALUE IS NOT NULL condition
410+ conditions .append ("JSON_VALUE(%s, '%s') IS NOT NULL" % (lhs , path_escaped ))
411+
412+ return _combine_conditions (conditions ), lhs_params
443413
444- # Return SQL with no params
445- return sql , []
446414
415+
447416def BinaryField_init (self , * args , ** kwargs ):
448417 # Add max_length option for BinaryField, default to max
449418 kwargs .setdefault ('editable' , False )
0 commit comments