Opened 3 weeks ago

Closed 13 days ago

Last modified 13 days ago

#37377 closed Bug (fixed)

JSONIn lookup crashes or returns wrong result for primitives

Reported by: Jacob Walls Owned by: Clifford Gama
Component: Database layer (models, ORM) Version: 6.1
Severity: Release blocker Keywords:
Cc: Clifford Gama Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

On databases besides Postgres (which has a native JSONField), a ​branch is taken that does not check the output_field for a Value expression. (Primitive strings, ints, bools, etc., might have other output_fields than JSONField.)

class Person(models.Model):
    data = models.JSONField(null=True)

def run():
    instance = Person.objects.create(data="primitive")
    qs = Person.objects.filter(data__in=[models.Value("primitive")])
    print(qs)

This query worked(*) on 6.0, but on 6.1 it returns the wrong result (MariaDB) or crashes (SQLite). Notice for SQLite here, the string "primitive" should not be provided to JSON_EXTRACT():

SELECT "app_person"."id", "app_person"."data" FROM "app_person" WHERE (CASE WHEN JSON_TYPE("app_person"."data", $) IN ('null','false','true') THEN JSON_TYPE("app_person"."data", $) ELSE JSON_EXTRACT("app_person"."data", $) END) IN (JSON_EXTRACT(primitive, '$'))
django.db.utils.OperationalError: malformed JSON

(*) -- On SQLite, you'd have to adjust the second query to use extra quotes, because of some quote weirdness, possibly similar to #32491?

Change History (6)

comment:1 by Clifford Gama, 3 weeks ago

Owner: set to Clifford Gama
Status: new → assigned

comment:2 by Clifford Gama, 3 weeks ago

Has patch: set
Triage Stage: Unreviewed → Accepted

Thanks Jacob for the report!

When I wrote the regression test, I found that the case for Boolean primitives wrapped in Value with no output_field was failing prior to refs #36689 which introduces the reported regression. I used Claude to run tests against different points in the history to get a clear picture of what was supported prior to #36689 and what wasn't. Here's what I found:

Value(<primitive>) in JSONField.__in — by commit and backend

Commit SQLite bool SQLite str SQLite num MySQL 8.4 bool MySQL 8.4 str MySQL 8.4 num PostgreSQL bool PostgreSQL str PostgreSQL num Oracle 23.5 bool Oracle 23.5 str Oracle 23.5 num
bbabbac936caf6db129427e6e65f03b6d0a68f62 (baseline, before #36689) FAIL FAIL PASS FAIL PASS PASS ERROR ERROR ERROR ERROR ERROR ERROR
66fed37ecb78daf0a50e95151a752b5760293514 (Fixed #36689 – adds JSONIn) FAIL PASS PASS FAIL PASS PASS ERROR ERROR ERROR ERROR PASS ERROR
9b8e4c6d7d47f7b2e96817ba334e9fd53c10a0df (Refs #36689 – introduces regression) FAIL ERROR PASS ERROR ERROR ERROR ERROR ERROR ERROR ERROR FAIL ERROR
7b54ddd5e64c96c641b70bb5f0e958a9e2035fb2 (Refs #36025) FAIL ERROR PASS ERROR ERROR ERROR ERROR ERROR ERROR ERROR FAIL ERROR
proposed fix (backportable?) PASS PASS PASS PASS PASS PASS ERROR ERROR ERROR PASS PASS PASS

Legend: PASS = correct row returned · FAIL = wrong result, silently returns [] (no exception) · ERROR = raises a DB-level exception.

Errors by cell, where relevant:

Caveats:

  • MySQL column is real MySQL 8.4 (mysql_is_mariadb=False_UNQUOTE()` branch was not exercised against an actualMariaDB server.
  • PostgreSQL ERROR in every row/commit is out of scope -- has_native_json_field=True skips this code path entirely regardless of commit, reflecting a separate, pre-existing gap (untyped Value() vs. jsonb operators) unrelated to #37377.
  • Oracle's numeric/boolean breakage did not start aty broken the moment 66fed37ecb78daf0a50e95151a752b5760293514 added JSONIn's OracleCOALESCE(JSON_VALUE(...), JSON_QUERY(...)) LHS extraction, because that always yields text, and an untyped Value()'s raw (non-text) bind collides with it once any row holds a container value. `9b8anged *how* Oracle failed for strings (silent wrong result →still wrong, no longer erroring) — they didn't newly break anything there.

comment:3 by Jacob Walls, 2 weeks ago

Patch needs improvement: set

comment:4 by Jacob Walls, 13 days ago

Patch needs improvement: unset
Triage Stage: Accepted → Ready for checkin

comment:5 by Jacob Walls <jacobtylerwalls@…>, 13 days ago

Resolution: → fixed
Status: assigned → closed

In fabce943:

Fixed #37377 -- Fixed JSONField in crash/wrong result for Value() primitives.

Refs #36689. 9b8e4c6d7d wrapped every Value() rhs param of a JSONIn lookup
in JSON_EXTRACT()/JSON_VALUE() assuming it was already JSON-serialized.
That only holds for Value(..., JSONField()); a bare Value("primitive") gets
an inferred non-JSON output_field (e.g. CharField), so its compiled param
was a raw primitive, not JSON text.

JSONIn.resolve_expression_parameter() now JSON-encodes the Value's raw
.value itself whenever its output_field isn't JSONField.

As a side effect, this also fixes Value(True)/Value(False) in in, which
were previously compared as raw Python bools against the JSON-extracted
lhs and never matched.

Thanks Jacob Walls for the report.

Regression in 9b8e4c6d7d47f7b2e96817ba334e9fd53c10a0df.

Co-authored-by: Jacob Walls <jacobtylerwalls@…>

comment:6 by Jacob Walls <jacobtylerwalls@…>, 13 days ago

In 71084d6:

[6.1.x] Fixed #37377 -- Fixed JSONField in crash/wrong result for Value() primitives.

Refs #36689. 9b8e4c6d7d wrapped every Value() rhs param of a JSONIn lookup
in JSON_EXTRACT()/JSON_VALUE() assuming it was already JSON-serialized.
That only holds for Value(..., JSONField()); a bare Value("primitive") gets
an inferred non-JSON output_field (e.g. CharField), so its compiled param
was a raw primitive, not JSON text.

JSONIn.resolve_expression_parameter() now JSON-encodes the Value's raw
.value itself whenever its output_field isn't JSONField.

As a side effect, this also fixes Value(True)/Value(False) in in, which
were previously compared as raw Python bools against the JSON-extracted
lhs and never matched.

Thanks Jacob Walls for the report.

Regression in 9b8e4c6d7d47f7b2e96817ba334e9fd53c10a0df.

Co-authored-by: Jacob Walls <jacobtylerwalls@…>

Backport of fabce94374e166e80e2643db8b85d1e3442427bf from main.

Note: See TracTickets for help on using tickets.
Back to Top