Pushkal-Gupta opened a new pull request, #75025:
URL: https://github.com/apache/airflow/pull/75025

   Both task instance list endpoints offer `data_interval_start` and 
`data_interval_end` as attributes you can order by, and both support cursor 
pagination. Combining the two fails.
   
   ```
   GET 
/api/v2/dags/~/dagRuns/~/taskInstances?cursor=&limit=2&order_by=data_interval_start
   
   AttributeError: 'TaskInstance' object has no attribute 'data_interval_start'
   NotImplementedError: Cursor pagination cannot resolve column-form 
``to_replace`` for
   ``data_interval_start``: the primary model exposes no such attribute. Add an 
association
   proxy, use a string alias, or sort by a primary-model column.
   ```
   
   Ordering itself is fine. Both names are `to_replace` entries pointing at 
`DagRun` columns, so the `ORDER BY` lands on the joined run. The failure is in 
encoding the cursor afterwards: `SortParam.row_value()` reads the sort key back 
off the result row, which is a `TaskInstance`, and neither name exists there. 
So the first page is fine until it has to hand out a `next_cursor`, which means 
the break only shows up once there is more than one page.
   
   `run_after` and `logical_date` are in the same `to_replace` map and do not 
break, because `TaskInstance` already exposes both as association proxies onto 
`dag_run`. These two were simply never added.
   
   ### The change
   
   Two more proxies alongside the existing ones, which is the fix the error 
message itself suggests:
   
   ```python
    run_after = association_proxy("dag_run", "run_after")
    logical_date = association_proxy("dag_run", "logical_date")
   +data_interval_start = association_proxy("dag_run", "data_interval_start")
   +data_interval_end = association_proxy("dag_run", "data_interval_end")
    team_name = association_proxy("dag_run", "team_name")
   ```
   
   No extra queries: the `dag_run` relationship is already `lazy="joined", 
innerjoin=True`, which is the same reason the two existing proxies are free.
   
   Nothing else picks these up. `TaskInstanceResponse` does not declare either 
name, so the API payload is unchanged and the spec does not move. I also 
checked there is no code keyed on their absence through `hasattr` or `getattr`.
   
   ### Tests
   
   Two cases, one per attribute, in `TestGetTaskInstances`. They page all the 
way through with `limit=2` and assert all five task instances come back exactly 
once, because a single request would have passed without exercising the encode 
at all.
   
   Paging alone turned out not to be enough of an assertion. All five task 
instances belong to one Dag run, so they tie on the data interval and the 
primary-key tie-break would page them correctly even if the interval had 
encoded as null. Worse, the shared fixture leaves the run's data interval NULL, 
so that is exactly what the first version of the test was doing. It now sets a 
real interval on the run and decodes the first `next_cursor` to check the 
boundary value went in, which is the part that was actually broken.
   
   Both fail without the change, with the `NotImplementedError` above. 
`test_task_instances.py` goes from 511 to 513 passed.
   
   ### Also looked at
   
   The other `to_replace` entries of this shape across `api_fastapi`. 
`map_index` and `rendered_map_index` resolve on `TaskInstance`, the 
`dag_run.py` sorts use the string form which takes a different and safe path in 
`row_value`, and `/dags` has two column-form entries but does not use cursor 
pagination, so it never reaches the encode.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes - Claude Code(Opus 5)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to