Skip to content

Commit 306d97e

Browse files
FEAT:Support 5.1-Modified JSON_Haskey_Lookup (#459)
* removed deprecated fields * modified json has_key lookup * Enhanced code quality * resolved conflict * refactored the code * refactored the json_haskey code * refactored haskey code * refactored the haskey function * refactored json function --------- Co-authored-by: Gaurav Sharma <[email protected]>
1 parent d5e56af commit 306d97e

File tree

1 file changed

+48
-79
lines changed

1 file changed

+48
-79
lines changed

mssql/functions.py

Lines changed: 48 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
304303
def 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
312309
def 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+
447416
def BinaryField_init(self, *args, **kwargs):
448417
# Add max_length option for BinaryField, default to max
449418
kwargs.setdefault('editable', False)

0 commit comments

Comments
 (0)