-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
Fixed #36134 -- Add support for ABSENT ON NULL clause for JSONArray. #19097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fixed #36134 -- Add support for ABSENT ON NULL clause for JSONArray. #19097
Conversation
5dcc39c
to
5f3ffa9
Compare
A few notes real quick: I chose to use an enum rather than a simple boolean keyword argument to mirror the SQL I put the enum on the JsonArray class, because I didn't think it had any use outside of JSON_ARRAY. I actually am seeing that's not true. It looks like JsonObject has a similar clause: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_OBJECT.html I could expand this pull request to add the functionality to JsonObject as well. I don't think it would be particularly difficult. Docs were a pretty rough copy/paste/tweak job. I added a new DB feature. I originally just did some basic checks inside the class, but it made it basically impossible to test cleanly. |
It looks like the biggest issue with adding an The closest parallel I can think of in the existing database is with |
11b044e
to
af250d9
Compare
As an aside, my version of JSONObject I made long ago didn't take So instead of JSONObject(
key="value"
) You would do JSONObject(
Value("key"), F("value")
) Which is obviously more verbose, but it let you create an object where one of the keys was actually dynamic from another column. It would also resolve the ambiguity of providing an I'm not exactly sure how that would resolve the issue with current django, though. Another thought would be to have an alternate constructor as a classmethod JSONObject.absent_on_null(**kwargs) Or having to manually set a prop. obj = JSONObject(...)
obj.absent_on_null = True But the prop is error-prone and requires you to create a temporary variable. I guess another solution is to just make it so you can't build objects with |
I'd focus this PR on A potential solution could be to require that fields are provided as a @overload
def __init__(self, fields: list[str, Resolvable], *, exclude_nulls: bool = False): ...
@overload
def __init__(self, **fields: Resolvable): ...
def __init__(self, *args, **kwargs):
if len(args) > 1:
raise TypeError
elif args:
fields = dict(args)
exclude_nulls = kwargs.pop("exclude_nulls", False)
if kwargs:
raise TypeError
else:
fields = kwargs It also seems like a less useful feature for |
Ack, I wish the type annotations were just in django. That's so clear. It's funny, I was hacking on pretty much exactly this solution , minus type annotations. |
|
||
return self.as_sql( | ||
compiler, | ||
connection, | ||
template=( | ||
f"%(function)s(%(expressions)s {null_on_null} RETURNING {returning})" | ||
f"%(function)s(%(expressions)s {on_null_clause} RETURNING {returning})" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is a VERY minor quibble, but if you don't have any source expressions, the generate SQL will end up having a triple space in it.
If the spaces were moved to lines 33 and 35, and then removed from the template here, it would resolve that, at the expense of making it slightly awkward right here.
Is that even something worth discussing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if I were hand-writing the SQL, I would write it like JSON_OBJECT(RETURNING JSONB)
, but the ORM will emit JSON_OBJECT( RETURNING JSONB)
I removed |
I think that the ability to provide key-value pairs to As an example esoteric usage, if you're trying to get the database to omit valid objects for the Elasticsearch DSL, you would almost certainly need to be able to build objects with non-static keys. For example https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html The key I think if you proposed this to me as a reasonable use of Django, I would think you're crazy, but that's one use off the top of my head. I can create a feature issue if desired. |
Looks like MySQL tests are failing. I think it's because it doesn't use the "as_native" definition where the code is. I'll take a closer look. |
Right, because MySQL doesn't support it. Duh. Toggled the value on the backend. Need to update the docs. I am wondering if a workaround for the non-native backends would be worth it. I know a lot of bending over backwards is done for some functions and backends. |
Alright, thought about it for a bit and decided the workaround wasn't as nasty as I thought it might be, at least for MySQL and Postgres 15. I have absolutely no idea how to do it on SQLite. I believe it would require a subquery and an aggregate, or something really, really weird. Probably not safe to do it automatically. If we go with this workaround version, I think we can just get rid of the |
Alright, last suite of changes:
|
Polishing:
|
Few extra tests added, tests reorganized a bit:
I think that's should be sufficient to exercise all of the branches in the workaround. |
c248d95
to
27d717d
Compare
Additional changes:
|
Note: I am still missing a release note. What version of Django would this be targeting? Should be able to make it into |
@john-parton 5.2 is feature frozen at this point so that would be 6.0. |
Added a release note. Tweaked the docs to have a I'm not the best at getting docs formatted exactly right, so some help there would be appreciated. |
I should probably stop iterating, but I'm a bit obsessed at the moment, apparently. The workaround that I had was pretty nasty in that mysql and postgres had to pass specific arguments to the method in order so that it could do the right concatenation operation. I just pulled out the concatenation logic into its own function, and then there's actually no database-specific logic in the main workaround now. I prepended an underscore to the function to indicate that it's private. If it's used incorrectly, it could cause pain. In particular, there's no attempt made to make sure that the expressions will actually resolve to arrays, and if you pass objects, the behavior is backend dependent. Pulling the concatenation into its own function does let me write tests directly against that, which I have done. |
2f94da9
to
340d4e6
Compare
Made the requested changes, rebased, force-pushed. I also updated the "Admonition" section of the docs to properly reflect that usage of It seems like a pretty bad hack to use json loads/dumps in the SQLite function. Bad code smell. Let me know if there's a more straightforward or better way of handling that. I ran tests locally against SQLite and Postgres. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the adjustments @john-parton, glad you figured out the SQLite function registration interface.
Left some potential pointers in how the loads/dumps roundtrip could be avoided on SQLite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for given the custom SQLite function a try. Kind of awesome how SQLite provides an interface for that right?
I left a suggestion for an additional test that ensures the null eliding is performed at the database level and not faked at the Python level when retrieving the tuples.
Otherwise the patch LGTM 🎉 Once all squashed and rebase use the past tense in the commit message (Add
-> Added
) and mark the patch as not needing improvements anymore.
713cc5c
to
661c779
Compare
Rebased, force-pushed. Summary of changes:
|
661c779
to
da32cb9
Compare
Does anyone know why tests failed? I'm unable to understand the error message. Can we retrigger tests? |
It's issues with the CI infra, force-pushing an amended commit should do assuming the problem was transient. |
da32cb9
to
5f8346a
Compare
Amended the commit and force-pushed. Checks are going now. |
We're getting Side-note: I really wish an exception here could provide a traceback or some more context. I don't have a Windows machine, so this makes it really difficult for me to continue development. |
5f8346a
to
735746e
Compare
I looked again, and there's not a dedicated Linux/SQLite entry in the test matrix. It was just SQLite failing due to a mistake on my part. I've fixed it, ran the test suite locally again to 100% be sure we're good to go, and pushed again. |
.. admonition:: MySQL and MariaDB | ||
|
||
MySQL and MariaDB don't support ``True`` for the ``absent_on_null`` parameter. | ||
|
||
.. admonition:: PostgreSQL | ||
|
||
``True`` for the ``absent_on_null`` parameter requires PostgreSQL version 16 | ||
or later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.. admonition:: MySQL and MariaDB | |
MySQL and MariaDB don't support ``True`` for the ``absent_on_null`` parameter. | |
.. admonition:: PostgreSQL | |
``True`` for the ``absent_on_null`` parameter requires PostgreSQL version 16 | |
or later. | |
.. admonition:: MySQL, MariaDB and PostgreSQL | |
The ``absent_on_null`` parameter is ignored with MySQL, MariaDB and | |
PostgreSQL < 16 as they do not support it. | |
.. versionchanged:: 6.0 | |
The ``absent_on_null`` parameter was added. |
Then remove the .. versionadded:: 6.0
note
We also need a release note in the 6.0 notes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your documentation here doesn't actually reflect the behavior. It's not actually ignored, it raises a NotSupportedError
. Is there a specific way I should phrase that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MySQL, MariaDB and PostgreSQL < 16 do not support the ``absent_on_null``
parameter.
perhaps 🤔
410e1db
to
7063b40
Compare
Please be patient while I un-break my fork. I foolishly decided to try to do something on my phone. |
3870596
to
fbf02c0
Compare
fbf02c0
to
21b8807
Compare
Trac ticket number
https://code.djangoproject.com/ticket/36134
Branch description
On all backends except Sqlite and Postgres version 15 and earlier, there is an additional clause to the JSON_ARRAY SQL which controls whether SQL NULL values are mapped to JSON null values or whether they are omitted.
In sqlite and postgres 15 and earlier, the default behavior is to map them, so my initial feature for JsonArray just had that as the only behavior without any ability to customize on the richer backends.
This adds a parameter to JsonArray, defaulting to the old behavior.
Checklist
main
branch.