|
16 | 16 | from django.db.models.lookups import In, Lookup
|
17 | 17 | from django.db.models.query import QuerySet
|
18 | 18 | from django.db.models.sql.query import Query
|
| 19 | +# import value and JSONArray for Django 5.2+ |
| 20 | +if VERSION >= (5, 2): |
| 21 | + from django.db.models import Value |
| 22 | + from django.db.models.functions import JSONArray |
19 | 23 |
|
20 | 24 | if VERSION >= (3, 1):
|
21 | 25 | from django.db.models.fields.json import (
|
@@ -225,6 +229,70 @@ def unquote_json_rhs(rhs_params):
|
225 | 229 | rhs_params = [param.replace('"', '') for param in rhs_params]
|
226 | 230 | return rhs_params
|
227 | 231 |
|
| 232 | +def sqlserver_json_array(self, compiler, connection, **extra_context): |
| 233 | + """ |
| 234 | + SQL Server implementation of JSONArray. |
| 235 | + """ |
| 236 | + elements = [] # List to hold SQL fragments for each array element |
| 237 | + params = [] # List to hold parameters for the SQL query |
| 238 | + |
| 239 | + # Iterate through each source expression (element of the array) |
| 240 | + for arg in self.source_expressions: |
| 241 | + # Check if the argument is a Value instance |
| 242 | + if isinstance(arg, Value): |
| 243 | + # If it's a Value, we need to handle it based on its type |
| 244 | + val = arg.value |
| 245 | + # If the value is None, we represent it as SQL NULL |
| 246 | + if val is None: |
| 247 | + elements.append('NULL') |
| 248 | + elif isinstance(val, (int, float)): |
| 249 | + # Numbers are inserted as it is, without quotes |
| 250 | + elements.append('%s') |
| 251 | + params.append(str(val)) |
| 252 | + elif isinstance(val, (list, dict)): |
| 253 | + # Nested JSON structures are handled with JSON_QUERY |
| 254 | + elements.append('JSON_QUERY(%s)') |
| 255 | + params.append(json.dumps(val)) |
| 256 | + else: |
| 257 | + # Strings and other types are cast to NVARCHAR(MAX) |
| 258 | + elements.append('CAST(%s AS NVARCHAR(MAX))') |
| 259 | + params.append(str(val)) |
| 260 | + else: |
| 261 | + # Compile non-Value expressions (e.g., fields, functions) |
| 262 | + arg_sql, arg_params = compiler.compile(arg) |
| 263 | + if isinstance(arg, JSONArray): |
| 264 | + # Nested JSONArray: use its SQL directly |
| 265 | + elements.append(arg_sql) |
| 266 | + else: |
| 267 | + # Other expressions: cast to NVARCHAR(MAX) |
| 268 | + elements.append(f'CAST({arg_sql} AS NVARCHAR(MAX))') |
| 269 | + if arg_params: |
| 270 | + params.extend(arg_params) |
| 271 | + # If there are no elements, return an empty JSON array |
| 272 | + if not elements: |
| 273 | + return "JSON_QUERY('[]')", [] |
| 274 | + |
| 275 | + # Build the SQL for the JSON array using STRING_AGG and CASE for formatting |
| 276 | + sql = ( |
| 277 | + "JSON_QUERY((" |
| 278 | + "SELECT '[' + " |
| 279 | + "STRING_AGG(" |
| 280 | + "CASE " |
| 281 | + "WHEN value IS NULL THEN 'null' " # NULLs as JSON null |
| 282 | + "WHEN ISJSON(value) = 1 THEN value " # Valid JSON: insert as-is |
| 283 | + "WHEN ISNUMERIC(value) = 1 THEN CAST(value AS NVARCHAR(MAX)) " # Numbers: insert as-is |
| 284 | + "ELSE CONCAT('\"', REPLACE(REPLACE(value, '\\', '\\\\'), '\"', '\\\"'), '\"') " # Strings: escape and quote |
| 285 | + "END, " |
| 286 | + "','" |
| 287 | + ") + ']' " |
| 288 | + f"FROM (VALUES {','.join('(' + el + ')' for el in elements)}) AS t(value)))" |
| 289 | + ) |
| 290 | + |
| 291 | + return sql, params |
| 292 | + |
| 293 | +# Register for Django 5.2+ so that JSONArray uses this implementation on SQL Server |
| 294 | +if VERSION >= (5, 2): |
| 295 | + JSONArray.as_microsoft = sqlserver_json_array |
228 | 296 |
|
229 | 297 | def json_KeyTransformExact_process_rhs(self, compiler, connection):
|
230 | 298 | rhs, rhs_params = key_transform_exact_process_rhs(self, compiler, connection)
|
|
0 commit comments