Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ jobs:

strategy:
matrix:
Python3.13 - Django 5.2:
python.version: '3.13'
tox.env: 'py313-django52'
Python3.12 - Django 5.2:
python.version: '3.12'
tox.env: 'py312-django52'
Python3.11 - Django 5.2:
python.version: '3.11'
tox.env: 'py311-django52'
Python3.10 - Django 5.2:
python.version: '3.10'
tox.env: 'py310-django52'

Python3.13 - Django 5.1:
python.version: '3.13'
tox.env: 'py313-django51'
Expand Down Expand Up @@ -151,6 +164,19 @@ jobs:

strategy:
matrix:
Python3.13 - Django 5.2:
python.version: '3.13'
tox.env: 'py313-django52'
Python3.12 - Django 5.2:
python.version: '3.12'
tox.env: 'py312-django52'
Python3.11 - Django 5.2:
python.version: '3.11'
tox.env: 'py311-django52'
Python3.10 - Django 5.2:
python.version: '3.10'
tox.env: 'py310-django52'

Python3.13 - Django 5.1:
python.version: '3.13'
tox.env: 'py313-django51'
Expand Down
68 changes: 68 additions & 0 deletions mssql/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
from django.db.models.lookups import In, Lookup
from django.db.models.query import QuerySet
from django.db.models.sql.query import Query
# import value and JSONArray for Django 5.2+
if VERSION >= (5, 2):
from django.db.models import Value
from django.db.models.functions import JSONArray

if VERSION >= (3, 1):
from django.db.models.fields.json import (
Expand Down Expand Up @@ -211,6 +215,70 @@ def unquote_json_rhs(rhs_params):
rhs_params = [param.replace('"', '') for param in rhs_params]
return rhs_params

def sqlserver_json_array(self, compiler, connection, **extra_context):
"""
SQL Server implementation of JSONArray.
"""
elements = [] # List to hold SQL fragments for each array element
params = [] # List to hold parameters for the SQL query

# Iterate through each source expression (element of the array)
for arg in self.source_expressions:
# Check if the argument is a Value instance
if isinstance(arg, Value):
# If it's a Value, we need to handle it based on its type
val = arg.value
# If the value is None, we represent it as SQL NULL
if val is None:
elements.append('NULL')
elif isinstance(val, (int, float)):
# Numbers are inserted as it is, without quotes
elements.append('%s')
params.append(str(val))
elif isinstance(val, (list, dict)):
# Nested JSON structures are handled with JSON_QUERY
elements.append('JSON_QUERY(%s)')
params.append(json.dumps(val))
else:
# Strings and other types are cast to NVARCHAR(MAX)
elements.append('CAST(%s AS NVARCHAR(MAX))')
params.append(str(val))
else:
# Compile non-Value expressions (e.g., fields, functions)
arg_sql, arg_params = compiler.compile(arg)
if isinstance(arg, JSONArray):
# Nested JSONArray: use its SQL directly
elements.append(arg_sql)
else:
# Other expressions: cast to NVARCHAR(MAX)
elements.append(f'CAST({arg_sql} AS NVARCHAR(MAX))')
if arg_params:
params.extend(arg_params)
# If there are no elements, return an empty JSON array
if not elements:
return "JSON_QUERY('[]')", []

# Build the SQL for the JSON array using STRING_AGG and CASE for formatting
sql = (
"JSON_QUERY(("
"SELECT '[' + "
"STRING_AGG("
"CASE "
"WHEN value IS NULL THEN 'null' " # NULLs as JSON null
"WHEN ISJSON(value) = 1 THEN value " # Valid JSON: insert as-is
"WHEN ISNUMERIC(value) = 1 THEN CAST(value AS NVARCHAR(MAX)) " # Numbers: insert as-is
"ELSE CONCAT('\"', REPLACE(REPLACE(value, '\\', '\\\\'), '\"', '\\\"'), '\"') " # Strings: escape and quote
"END, "
"','"
") + ']' "
f"FROM (VALUES {','.join('(' + el + ')' for el in elements)}) AS t(value)))"
)

return sql, params

# Register for Django 5.2+ so that JSONArray uses this implementation on SQL Server
if VERSION >= (5, 2):
JSONArray.as_microsoft = sqlserver_json_array

def json_KeyTransformExact_process_rhs(self, compiler, connection):
rhs, rhs_params = key_transform_exact_process_rhs(self, compiler, connection)
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Framework :: Django :: 5.1',
'Framework :: Django :: 5.2',
]

this_directory = path.abspath(path.dirname(__file__))
Expand All @@ -43,7 +45,7 @@
license='BSD',
packages=find_packages(),
install_requires=[
'django>=3.2,<5.2',
'django>=3.2,<5.3',
'pyodbc>=3.0',
'pytz',
],
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ envlist =
{py38, py39, py310}-django42,
{py310, py311, py312}-django50
{py310, py311, py312,py313}-django51
{py310, py311, py312,py313}-django52

[testenv]
allowlist_externals =
Expand All @@ -25,3 +26,4 @@ deps =
django42: django>=4.2,<4.3
django50: django>=5.0,<5.1
django51: django>=5.1,<5.2
django52: django>=5.2,<5.3
Loading