henry3260 commented on code in PR #70552:
URL: https://github.com/apache/airflow/pull/70552#discussion_r3663922967


##########
airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py:
##########
@@ -151,42 +155,24 @@ def get_assets(
     only_active: Annotated[OnlyActiveFilter, 
Depends(OnlyActiveFilter.depends)],
     order_by: Annotated[
         SortParam,
-        Depends(SortParam(["id", "name", "uri", "created_at", "updated_at"], 
AssetModel).dynamic_depends()),
+        Depends(
+            SortParam(
+                ["id", "name", "uri", "created_at", "updated_at", 
"last_asset_event_timestamp"],
+                AssetModel,
+                to_replace={
+                    "last_asset_event_timestamp": 
_last_asset_event_query.c.last_asset_event_timestamp,
+                },
+            ).dynamic_depends()
+        ),
     ],
     session: SessionDep,
 ) -> AssetCollectionResponse:
     """Get assets."""
-    # Build a query that will be used to retrieve the ID and timestamp of the 
latest AssetEvent
-    last_asset_events = (
-        select(AssetEvent.asset_id, 
func.max(AssetEvent.timestamp).label("last_timestamp"))
-        .group_by(AssetEvent.asset_id)
-        .subquery()
-    )
-
-    # First, we're pulling the Asset ID, AssetEvent ID, and AssetEvent 
timestamp for the latest (last)
-    # AssetEvent. We'll eventually OUTER JOIN this to the AssetModel
-    asset_event_query = (
-        select(
-            AssetEvent.asset_id,  # The ID of the Asset, which we'll need to 
JOIN to the AssetModel
-            func.max(AssetEvent.id).label("last_asset_event_id"),  # The ID of 
the last AssetEvent
-            func.max(AssetEvent.timestamp).label("last_asset_event_timestamp"),
-        )
-        .join(
-            last_asset_events,
-            and_(
-                AssetEvent.asset_id == last_asset_events.c.asset_id,
-                AssetEvent.timestamp == last_asset_events.c.last_timestamp,
-            ),
-        )
-        .group_by(AssetEvent.asset_id)
-        .subquery()
-    )
-
     assets_select_statement = select(
         AssetModel,
-        asset_event_query.c.last_asset_event_id,  # This should be the 
AssetEvent.id
-        asset_event_query.c.last_asset_event_timestamp,
-    ).outerjoin(asset_event_query, AssetModel.id == 
asset_event_query.c.asset_id)
+        _last_asset_event_query.c.last_asset_event_id,  # This should be the 
AssetEvent.id

Review Comment:
   ```suggestion
           _last_asset_event_query.c.last_asset_event_id,  
   ```



##########
airflow-core/src/airflow/api_fastapi/common/db/assets.py:
##########
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from sqlalchemy import and_, func, select
+
+from airflow.models.asset import AssetEvent
+
+if TYPE_CHECKING:
+    from sqlalchemy.sql import Subquery
+
+
+def generate_last_asset_event_query() -> Subquery:
+    """Build a subquery yielding the ID and timestamp of the latest AssetEvent 
per asset."""
+    last_asset_event_per_asset = (
+        select(AssetEvent.asset_id, 
func.max(AssetEvent.timestamp).label("last_timestamp"))
+        .group_by(AssetEvent.asset_id)
+        .subquery()
+    )
+
+    return (
+        select(
+            AssetEvent.asset_id,  # The ID of the Asset, which we'll need to 
JOIN to the AssetModel
+            func.max(AssetEvent.id).label("last_asset_event_id"),  # The ID of 
the last AssetEvent

Review Comment:
   ```suggestion
               func.max(AssetEvent.id).label("last_asset_event_id"),  



##########
airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py:
##########
@@ -497,6 +497,20 @@ def test_order_by_raises_400_for_invalid_attr(self, 
test_client, session):
         msg = "Ordering with 'fake' is disallowed or the attribute does not 
exist on the model"
         assert response.json()["detail"] == msg
 
+    def test_order_by_last_asset_event_timestamp(self, test_client, session):

Review Comment:
   The sort directions are asserted, but I don't think this test can 
distinguish the new sort key from the primary-key fallback.
   
   `_create_assets_events(varying_timestamps=True)` assigns
   `timestamp=DEFAULT_DATE + timedelta(days=i - 1)` to `asset_id=i`, so 
timestamp order is identical to id order. `SortParam._resolve()` always appends 
the primary key as a tiebreaker (in the same direction as the first key), so 
`order_by=id` / `-id` produce exactly the same `[1, 2, 3]` / `[3, 2, 1]`. If 
the `to_replace` mapping pointed at the wrong column — or were dropped entirely 
— these assertions would still pass.
   
   Could we make the timestamps non-monotonic asset id, so the expected
   order differs from both id order and its reverse? e.g. offsets `[2, 0, 1]`, 
giving `[2, 3, 1]` ascending and `[1, 3, 2]` descending.



##########
airflow-core/src/airflow/api_fastapi/common/db/assets.py:
##########
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from sqlalchemy import and_, func, select
+
+from airflow.models.asset import AssetEvent
+
+if TYPE_CHECKING:
+    from sqlalchemy.sql import Subquery
+
+
+def generate_last_asset_event_query() -> Subquery:
+    """Build a subquery yielding the ID and timestamp of the latest AssetEvent 
per asset."""
+    last_asset_event_per_asset = (
+        select(AssetEvent.asset_id, 
func.max(AssetEvent.timestamp).label("last_timestamp"))
+        .group_by(AssetEvent.asset_id)
+        .subquery()
+    )
+
+    return (
+        select(
+            AssetEvent.asset_id,  # The ID of the Asset, which we'll need to 
JOIN to the AssetModel

Review Comment:
   ```suggestion
               AssetEvent.asset_id,  
   ```



-- 
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