diff --git a/discovery-provider/src/api/v1/users.py b/discovery-provider/src/api/v1/users.py index 02578367ea3..e21acd746f2 100644 --- a/discovery-provider/src/api/v1/users.py +++ b/discovery-provider/src/api/v1/users.py @@ -946,7 +946,7 @@ def get(self, id: str): class GetSupporters(Resource): @record_metrics @ns.doc( - id="""Get User Supporters""", + id="""Get Supporters""", description="""Gets the supporters of the given user""", params={"id": "A User ID"}, ) @@ -970,17 +970,17 @@ def get(self, id: str): @full_ns.route("//supporters") class FullGetSupporters(Resource): @record_metrics - @ns.doc( - id="""Get User Supporters""", + @full_ns.doc( + id="""Get Supporters""", description="""Gets the supporters of the given user""", params={"id": "A User ID"}, ) - @ns.expect(pagination_with_current_user_parser) - @ns.marshal_with(full_get_supporters_response) + @full_ns.expect(pagination_with_current_user_parser) + @full_ns.marshal_with(full_get_supporters_response) @cache(ttl_sec=5) def get(self, id: str): - args = pagination_parser.parse_args() - decoded_id = decode_with_abort(id, ns) + args = pagination_with_current_user_parser.parse_args() + decoded_id = decode_with_abort(id, full_ns) current_user_id = get_current_user_id(args) args["user_id"] = decoded_id args["current_user_id"] = current_user_id @@ -989,16 +989,47 @@ def get(self, id: str): return success_response(support) +full_get_supporter_response = make_full_response( + "full_get_supporter", full_ns, fields.Nested(supporter_response_full) +) + + +@full_ns.route("//supporters/") +class FullGetSupporter(Resource): + @record_metrics + @full_ns.doc( + id="""Get Supporter""", + description="""Gets the specified supporter of the given user""", + params={"id": "A User ID", "supporter_user_id": "A User ID of a supporter"}, + ) + @full_ns.expect(current_user_parser) + @full_ns.marshal_with(full_get_supporter_response) + @cache(ttl_sec=5) + def get(self, id: str, supporter_user_id: str): + args = current_user_parser.parse_args() + decoded_id = decode_with_abort(id, full_ns) + current_user_id = get_current_user_id(args) + decoded_supporter_user_id = decode_with_abort(supporter_user_id, full_ns) + args["user_id"] = decoded_id + args["current_user_id"] = current_user_id + args["supporter_user_id"] = decoded_supporter_user_id + support = get_support_received_by_user(args) + support = list(map(extend_supporter, support)) + if not support: + abort_not_found(supporter_user_id, full_ns) + return success_response(support[0]) + + get_supporting_response = make_response( "get_supporting", ns, fields.List(fields.Nested(supporting_response)) ) @ns.route("//supporting") -class GetSupporting(Resource): +class GetSupportings(Resource): @record_metrics @ns.doc( - id="""Get User Supporting""", + id="""Get Supportings""", description="""Gets the users that the given user supports""", params={"id": "A User ID"}, ) @@ -1020,10 +1051,10 @@ def get(self, id: str): @full_ns.route("//supporting") -class FullGetSupporting(Resource): +class FullGetSupportings(Resource): @record_metrics @full_ns.doc( - id="""Get User Supporting""", + id="""Get Supportings""", description="""Gets the users that the given user supports""", params={"id": "A User ID"}, ) @@ -1041,6 +1072,40 @@ def get(self, id: str): return success_response(support) +full_get_supporting_response = make_full_response( + "full_get_supporting", full_ns, fields.Nested(supporting_response_full) +) + + +@full_ns.route("//supporting/") +class FullGetSupporting(Resource): + @record_metrics + @full_ns.doc( + id="""Get Supporting""", + description="""Gets the support from the given user to the supported user""", + params={ + "id": "A User ID", + "supported_user_id": "A User ID of a supported user", + }, + ) + @full_ns.expect(current_user_parser) + @full_ns.marshal_with(full_get_supporting_response) + @cache(ttl_sec=5) + def get(self, id: str, supported_user_id: str): + args = current_user_parser.parse_args() + decoded_id = decode_with_abort(id, full_ns) + current_user_id = get_current_user_id(args) + decoded_supported_user_id = decode_with_abort(supported_user_id, full_ns) + args["user_id"] = decoded_id + args["current_user_id"] = current_user_id + args["supported_user_id"] = decoded_supported_user_id + support = get_support_sent_by_user(args) + support = list(map(extend_supporting, support)) + if not support: + abort_not_found(decoded_id, full_ns) + return success_response(support[0]) + + verify_token_response = make_response( "verify_token", ns, fields.List(fields.Nested(decoded_user_token)) ) diff --git a/discovery-provider/src/queries/get_support_for_user.py b/discovery-provider/src/queries/get_support_for_user.py index e11c3d02579..844ac0d4aa7 100644 --- a/discovery-provider/src/queries/get_support_for_user.py +++ b/discovery-provider/src/queries/get_support_for_user.py @@ -1,10 +1,14 @@ +import logging from typing import Any, Dict, List, Tuple, TypedDict -from sqlalchemy import Integer, column, text +from sqlalchemy import func +from sqlalchemy.orm import aliased from src.models import AggregateUserTips -from src.queries.query_helpers import get_users_by_id +from src.queries.query_helpers import get_users_by_id, paginate_query from src.utils.db_session import get_db_read_replica +logger = logging.getLogger(__name__) + class SupportResponse(TypedDict): rank: int @@ -27,43 +31,85 @@ def query_result_to_support_response( ] -sql_support_received = text( - """ -SELECT - RANK() OVER (ORDER BY amount DESC) AS rank - , sender_user_id - , receiver_user_id - , amount -FROM aggregate_user_tips -WHERE receiver_user_id = :receiver_user_id -ORDER BY amount DESC -LIMIT :limit -OFFSET :offset; -""" -).columns( - column("rank", Integer), - AggregateUserTips.sender_user_id, - AggregateUserTips.receiver_user_id, - AggregateUserTips.amount, -) +# Without supporter_user_id: +# ---------------------------- +# SELECT +# rank() OVER ( +# ORDER BY +# aggregate_user_tips.amount DESC +# ) AS rank, +# aggregate_user_tips.sender_user_id AS aggregate_user_tips_sender_user_id, +# aggregate_user_tips.receiver_user_id AS aggregate_user_tips_receiver_user_id, +# aggregate_user_tips.amount AS aggregate_user_tips_amount +# FROM +# aggregate_user_tips +# WHERE +# aggregate_user_tips.receiver_user_id = %(receiver_user_id_1) s +# ORDER BY +# aggregate_user_tips.amount DESC, aggregate_user_tips.sender_user_id ASC +# LIMIT +# %(param_1) s OFFSET %(param_2) s + + +# With supporter_user_id: +# ---------------------------- +# WITH rankings AS ( +# SELECT +# rank() OVER ( +# ORDER BY +# aggregate_user_tips.amount DESC +# ) AS rank, +# aggregate_user_tips.sender_user_id AS sender_user_id, +# aggregate_user_tips.receiver_user_id AS receiver_user_id, +# aggregate_user_tips.amount AS amount +# FROM +# aggregate_user_tips +# WHERE +# aggregate_user_tips.receiver_user_id = %(receiver_user_id_1) s +# ) +# SELECT +# rankings.rank AS rankings_rank, +# rankings.sender_user_id AS rankings_sender_user_id, +# rankings.receiver_user_id AS rankings_receiver_user_id, +# rankings.amount AS rankings_amount +# FROM +# rankings +# WHERE +# rankings.sender_user_id = %(sender_user_id_1) s def get_support_received_by_user(args) -> List[SupportResponse]: support: List[SupportResponse] = [] receiver_user_id = args.get("user_id") current_user_id = args.get("current_user_id") - limit = args.get("limit", 100) - offset = args.get("offset", 0) + supporter_user_id = args.get("supporter_user_id", None) db = get_db_read_replica() with db.scoped_session() as session: - query = ( - session.query("rank", AggregateUserTips) - .from_statement(sql_support_received) - .params(receiver_user_id=receiver_user_id, limit=limit, offset=offset) - ) - rows: List[Tuple[int, AggregateUserTips]] = query.all() + query = session.query( + func.rank().over(order_by=AggregateUserTips.amount.desc()).label("rank"), + AggregateUserTips, + ).filter(AggregateUserTips.receiver_user_id == receiver_user_id) + + # Filter to supporter we care about after ranking + if supporter_user_id is not None: + rankings = query.cte(name="rankings") + RankingsAggregateUserTips = aliased( + AggregateUserTips, rankings, name="aliased_rankings_tips" + ) + query = ( + session.query(rankings.c.rank, RankingsAggregateUserTips) + .select_from(rankings) + .filter(RankingsAggregateUserTips.sender_user_id == supporter_user_id) + ) + # Only paginate if not looking for single supporter + else: + query = query.order_by( + AggregateUserTips.amount.desc(), AggregateUserTips.sender_user_id.asc() + ) + query = paginate_query(query) + rows: List[Tuple[int, AggregateUserTips]] = query.all() user_ids = [row[1].sender_user_id for row in rows] users = get_users_by_id(session, user_ids, current_user_id) @@ -71,48 +117,123 @@ def get_support_received_by_user(args) -> List[SupportResponse]: return support -sql_support_sent = text( - """ -SELECT rank, sender_user_id, receiver_user_id, amount -FROM ( - SELECT - RANK() OVER (PARTITION BY B.receiver_user_id ORDER BY B.amount DESC) AS rank - , B.sender_user_id - , B.receiver_user_id - , B.amount - FROM aggregate_user_tips A - JOIN aggregate_user_tips B ON A.receiver_user_id = B.receiver_user_id - WHERE A.sender_user_id = :sender_user_id -) rankings -WHERE sender_user_id = :sender_user_id -ORDER BY amount DESC, receiver_user_id ASC -LIMIT :limit -OFFSET :offset; -""" -).columns( - column("rank", Integer), - AggregateUserTips.sender_user_id, - AggregateUserTips.receiver_user_id, - AggregateUserTips.amount, -) +# Without supported_user_id: +# ---------------------------- +# SELECT +# rankings.rank AS rankings_rank, +# rankings.sender_user_id AS rankings_sender_user_id, +# rankings.receiver_user_id AS rankings_receiver_user_id, +# rankings.amount AS rankings_amount +# FROM +# ( +# SELECT +# rank() OVER ( +# PARTITION BY joined_aggregate_tips.receiver_user_id +# ORDER BY joined_aggregate_tips.amount DESC +# ) AS rank, +# joined_aggregate_tips.sender_user_id AS sender_user_id, +# joined_aggregate_tips.receiver_user_id AS receiver_user_id, +# joined_aggregate_tips.amount AS amount +# FROM +# aggregate_user_tips +# JOIN +# aggregate_user_tips AS joined_aggregate_tips +# ON joined_aggregate_tips.receiver_user_id = aggregate_user_tips.receiver_user_id +# WHERE +# aggregate_user_tips.sender_user_id = % (sender_user_id_1)s +# ) +# AS rankings +# WHERE +# rankings.sender_user_id = % (sender_user_id_2)s +# ORDER BY +# rankings.amount DESC, +# rankings.receiver_user_id ASC LIMIT % (param_1)s OFFSET % (param_2)s + + +# With supported_user_id: +# ---------------------------- +# SELECT +# rankings.rank AS rankings_rank, +# rankings.sender_user_id AS rankings_sender_user_id, +# rankings.receiver_user_id AS rankings_receiver_user_id, +# rankings.amount AS rankings_amount +# FROM +# ( +# SELECT +# rank() OVER ( +# PARTITION BY joined_aggregate_tips.receiver_user_id +# ORDER BY joined_aggregate_tips.amount DESC +# ) AS rank, +# joined_aggregate_tips.sender_user_id AS sender_user_id, +# joined_aggregate_tips.receiver_user_id AS receiver_user_id, +# joined_aggregate_tips.amount AS amount +# FROM +# aggregate_user_tips +# JOIN +# aggregate_user_tips AS joined_aggregate_tips +# ON joined_aggregate_tips.receiver_user_id = aggregate_user_tips.receiver_user_id +# WHERE +# aggregate_user_tips.sender_user_id = % (sender_user_id_1)s +# AND aggregate_user_tips.receiver_user_id = % (receiver_user_id_1)s +# ) +# AS rankings +# WHERE +# rankings.sender_user_id = % (sender_user_id_2)s def get_support_sent_by_user(args) -> List[SupportResponse]: support: List[SupportResponse] = [] sender_user_id = args.get("user_id") current_user_id = args.get("current_user_id") - limit = args.get("limit") - offset = args.get("offset") + supported_user_id = args.get("supported_user_id", None) db = get_db_read_replica() with db.scoped_session() as session: + AggregateUserTipsB = aliased(AggregateUserTips, name="joined_aggregate_tips") query = ( - session.query("rank", AggregateUserTips) - .from_statement(sql_support_sent) - .params(sender_user_id=sender_user_id, limit=limit, offset=offset) + session.query( + func.rank() + .over( + partition_by=AggregateUserTipsB.receiver_user_id, + order_by=AggregateUserTipsB.amount.desc(), + ) + .label("rank"), + AggregateUserTipsB, + ) + .select_from(AggregateUserTips) + .join( + AggregateUserTipsB, + AggregateUserTipsB.receiver_user_id + == AggregateUserTips.receiver_user_id, + ) + .filter(AggregateUserTips.sender_user_id == sender_user_id) ) - rows: List[Tuple[int, AggregateUserTips]] = query.all() + # Filter to the receiver we care about early + if supported_user_id is not None: + query = query.filter( + AggregateUserTips.receiver_user_id == supported_user_id + ) + + subquery = query.subquery(name="rankings") + AggregateUserTipsAlias = aliased( + AggregateUserTips, subquery, name="aggregate_user_tips_alias" + ) + query = ( + session.query(subquery.c.rank, AggregateUserTipsAlias) + .select_from(subquery) + .filter(AggregateUserTipsAlias.sender_user_id == sender_user_id) + ) + + # Only paginate if not looking for single supporting + if supported_user_id is None: + query = query.order_by( + AggregateUserTipsAlias.amount.desc(), + AggregateUserTipsAlias.receiver_user_id.asc(), + ) + query = paginate_query(query) + + rows: List[Tuple[int, AggregateUserTips]] = query.all() user_ids = [row[1].receiver_user_id for row in rows] users = get_users_by_id(session, user_ids, current_user_id)