Add support for table value expressions in ORM - #113
Conversation
a725312 to
32a2c14
Compare
charettes
left a comment
There was a problem hiding this comment.
I left a few comments but overall the design document seems a like good foundation for further demonstration work of the viability of the approach.
I remain unconvinced of the necessity of the TableExpression abstraction as I believe isinstance(expr, CompositeField) (or a potential future CompositeField type narrowing Field.is_composite -> bool guard) is likely a good heuristic to fit our needs of augmenting sql.Query.alias_map. I believe this the case due to the complexity that isinstance(expr, (Subquery, QuerySet, sql.Query)) caused when Subquery was introduced (e.g. __in lookups).
| We will take inspiration from the existing `FilteredRelation`. We would follow the | ||
| mechanism it used to register itself as a table source in the ``FROM`` clause. | ||
|
|
||
| This DEP uses ``TableExpression`` as a placeholder name. The final public API | ||
| may use a different name. |
There was a problem hiding this comment.
I suspect we won't need TableExpression at all as we'll be able to perform the FROM aliasing based on other heuristics.
For example, in the case of QuerySet.resolve_expression if there are more than one field than it's output_field would be a CompositeField instance and we could make GenerateSeries.output_field = CompositeField(IntegerField()) even if it's a singleton.
The heuristic to add a member to sql.Query.alias_map could then be isinstance(output_field, CompositeField).
There was a problem hiding this comment.
We were also was in the favor of not adding TableExpression or something. But what we have discovered that .alias_map requires a Table-like object. and it was failing on the line
j for j in self.alias_map if self.alias_map[j].join_type == INNER
Query or Subquery don't have table_name, table_alias, join_type, parent_alias which would require to live inside alias_map as per the contract.
we might don't need objects.alias(TableExpression(Subquery(...)) instead
if isinstance(expr.output_field, CompositeField):
alias_map[alias] =TableExpression(expr)
also query object as_sql will generate something (SELECT ... FROM ...) i'm assuming we might need something in prefix or suffix in the FROM clause with another table. for that also we need some handling.
There was a problem hiding this comment.
I'm not sure I understand why the need for objects in alias_map to have certain attributes requires that objects passed to QuerySet.alias be TableExpression.
Why can't alias do something like
if isinstance(expr.output_field, CompositeField):
join = Join(
expr,
alias,
...
)
self.join(join)In other words, shouldn't we adapt Join and BaseTable to be more expression like and allow composition of other Expression over introducing TableExpression to bridge between them?
There was a problem hiding this comment.
I overlooked that this version of the DEP still suggested that TableExpression was going to be exposed to users. As you mentioned in #113 (comment) with the analogy to Subquery, it should be unnecessary in most instances, perhaps only if you need to specify a special kind of join?
We were just following the implementation hint in https://code.djangoproject.com/ticket/37115#comment:8 to create a base class for BaseTable and Join.
requires that objects passed to QuerySet.alias be TableExpression.
I think @p-r-a-v-i-n is aware that we don't want users to have to supply these wrappers, see #113 (comment), but we definitely need to update the DEP, because it suggests Simon's reading.
In other words, shouldn't we adapt Join and BaseTable to be more expression like and allow composition of other Expression over introducing TableExpression to bridge between them?
I think this is where we're all heading, let's discuss once there's an implementation PR to look at, which I think is imminent.
There was a problem hiding this comment.
it should be unnecessary in most instances, perhaps only if you need to specify a special kind of join?
right but I wonder if this should be a concern of this work or not, I think that laying down the foundation for table-like expressions is already a ton of work in itself.
I think this is where we're all heading, let's discuss once there's an implementation PR to look at, which I think is imminent.
Sounds good! Peeking at code will definitely help here.
There was a problem hiding this comment.
The question about join type came up right away in the first cut of the implementation, any guidance here @charettes and @LilyFirefly would be very helpful!
| For single-column table expressions, the final API may allow a shorter form such | ||
| as ``series__gt=1``. That detail is still open. |
There was a problem hiding this comment.
That should be relatively straightforward to support by having CompositeField.get_lookup use it's only field when it's the case.
| An early implementation could accept column metadata directly: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| TableExpression( | ||
| JsonEach("payload"), | ||
| columns={ | ||
| "key": models.TextField(), | ||
| "value": models.JSONField(), | ||
| }, | ||
| ) | ||
|
|
||
| This may be easy to prototype, but it creates a separate metadata path from | ||
| ``Expression.output_field``. |
There was a problem hiding this comment.
Not convinced this is warranted or not something that could not be provided by JSONEach directly.
e.g.
JsonEach("payload", key_column="attr")could be turned into
(SELECT key AS "attr", value FROM json_each("payload"))
yes obstacles I see without Table-Expression
|
|
Hi! Hope you're doing well! |
|
(Above, @p-r-a-v-i-n is using "spacesuit" as a colloquialism for "protocol".) |
I encouraged @p-r-a-v-i-n to defer that aspect for now, but we can re-prioritize it if you think it's important to include earlier. Can you say a little bit about the use cases for that? |
I think it's smart to defer the implementation as it should cut scope significantly. I think that making sure
From my understanding that was the original intent behind #17279 / ticket-373 but we took a shortcut to focus solely on composite primary key at first (leaving us with the composite fk problem for example). There was a SoC project for |
There's definitely some overlap, but this DEP is more about laying the groundwork for a wide collection of features, including CTEs. If this GSOC work goes well, you may be able to rewrite #106 to build on top of this work. |
| Expression-like table sources | ||
| ----------------------------- | ||
| After output columns can be represented, the ORM needs a way to explicitly register an expression or query as a table source in the ``FROM`` clause. | ||
| We cannot be sure if subqueries return multi-column single row or multi-column multi-row. | ||
| For example: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| subquery = Post.objects.filter(user=first_user).values("title", "body") | ||
| User.objects.alias(posts=subquery).values("posts__title", "posts__body") | ||
|
|
||
| In this example, the ORM cannot reliably know if the ``subquery`` returns exactly one row or multiple rows. | ||
| To solve this, our approach requires the developer to explicitly declare when a result is a multi-row table source. | ||
| The user will do this by wrapping the queryset in a ``TableExpression``. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| User.objects.alias(posts=TableExpression(subquery)) | ||
|
|
||
| This explicit wrapper allows the ORM to cleanly separate the processing logic. When encountering a ``TableExpression``, the ORM knows to place the subquery inside the ``FROM`` clause (utilizing ``LATERAL`` joins if there are outer references) rather than resolving it as a scalar subquery in the ``SELECT`` clause. | ||
| *(Note: This DEP uses ``TableExpression`` as a placeholder name. The final public API may use a different name).* |
There was a problem hiding this comment.
Please don't mind for writing very late and the reason is i'm being shy to write , I'm afraid of if I sound like "I'm forcing something or being very concrete on something".
I have try to add clarity in this section why do we might need TableExpression wrapper.
There was a problem hiding this comment.
Wouldn't the scalar single row case look like: (note the [:1])
subquery = Post.objects.filter(user=first_user).values("title", "body")[:1]
User.objects.alias(posts=subquery).values("posts__title", "posts__body")Or am I missing something here? (I might be!)
There was a problem hiding this comment.
Please correct me, I'm assuming you doubt that i have used similar example with [:1] in scaler case.
below is just examples (my understanding) , so we both are on same page
subquery = Post.objects.filter(user=first_user).values("title")[:1]
This one is scaler.
subquery = Post.objects.filter(user=first_user).values("title", "body")[:1]
This is multi-column single row
subquery = Post.objects.filter(user=first_user).values("title", "body")
If first_user has created multiple posts the this will return multi-column multi-row
i used this so it represent the table-value result
There was a problem hiding this comment.
Ah, so @LilyFirefly is your idea that we can inspect whether the queryset is sliced, and if not, treat it as multi-row, without the need for a wrapper?
There was a problem hiding this comment.
I think it's consistent with how we've designed subqueries up to this point and I think it's worth exploring if it will work in practice or cause problems (maybe it's too subtle and people will get confused?).
There was a problem hiding this comment.
I like it -- let's explore it.
This DEP proposes the implementation of a CompositeField within the Django ORM to support Table-Valued Expressions (TVEs) and functions that return sets of rows.
Refereneces:
- https://forum.djangoproject.com/t/proposal-add-generate-series-support-to-contrib-postgres/21947/4