@@ -104,7 +104,7 @@ class QueryResult:
104104
105105
106106class _ScoreParseMixin :
107- """Shared helper for detecting RETURN 0 (NOCONTENT) in translated args ."""
107+ """Shared helpers for score-related response parsing ."""
108108
109109 @staticmethod
110110 def _has_return_0 (args : list [str ]) -> bool :
@@ -115,6 +115,23 @@ def _has_return_0(args: list[str]) -> bool:
115115 except (ValueError , IndexError ):
116116 return False
117117
118+ @staticmethod
119+ def _resolve_score_alias (score_alias : str | None , args : list [str ]) -> str :
120+ """Determine a stable score column name that won't collide with
121+ document fields. The alias is decided once before iterating rows
122+ so every row uses the same column name."""
123+ alias = score_alias or "__score"
124+ # Extract RETURN field names from args to detect collision
125+ try :
126+ idx = args .index ("RETURN" )
127+ count = int (args [idx + 1 ])
128+ return_fields = set (args [idx + 2 : idx + 2 + count ])
129+ except (ValueError , IndexError ):
130+ return_fields = set ()
131+ if alias in return_fields :
132+ alias = f"__score_{ alias } "
133+ return alias
134+
118135
119136class Executor (_ScoreParseMixin ):
120137 """Executes SQL queries against Redis."""
@@ -181,17 +198,21 @@ def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
181198 if translated .command == "FT.SEARCH" :
182199 # Check if WITHSCORES was requested — changes response format
183200 with_scores = "WITHSCORES" in translated .args
184- score_alias = translated .score_alias
185201 # RETURN 0 suppresses document fields (like NOCONTENT);
186202 # with WITHSCORES the reply is [count, id, score, id, score, ...]
187203 no_content = self ._has_return_0 (translated .args )
204+ # Determine score column name once so every row is consistent
205+ alias = (
206+ self ._resolve_score_alias (translated .score_alias , translated .args )
207+ if with_scores
208+ else ""
209+ )
188210
189211 if with_scores and no_content :
190212 # WITHSCORES + RETURN 0: [count, id1, score1, id2, score2, ...]
191213 # Stride of 2: key, score (no field array)
192214 for i in range (1 , len (raw_result ) - 1 , 2 ):
193215 score = raw_result [i + 1 ]
194- alias = score_alias or "__score"
195216 row = {alias : score }
196217 rows .append (row )
197218 elif with_scores :
@@ -201,10 +222,6 @@ def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
201222 score = raw_result [i + 1 ]
202223 row_data = raw_result [i + 2 ]
203224 row = dict (zip (row_data [::2 ], row_data [1 ::2 ]))
204- alias = score_alias or "__score"
205- # Guard: if alias collides with a document field, prefix it
206- if alias in row :
207- alias = f"__score_{ alias } "
208225 row [alias ] = score
209226 rows .append (row )
210227 else :
@@ -294,14 +311,17 @@ async def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
294311
295312 if translated .command == "FT.SEARCH" :
296313 with_scores = "WITHSCORES" in translated .args
297- score_alias = translated .score_alias
298314 no_content = self ._has_return_0 (translated .args )
315+ alias = (
316+ self ._resolve_score_alias (translated .score_alias , translated .args )
317+ if with_scores
318+ else ""
319+ )
299320
300321 if with_scores and no_content :
301322 # WITHSCORES + RETURN 0: [count, id1, score1, id2, score2, ...]
302323 for i in range (1 , len (raw_result ) - 1 , 2 ):
303324 score = raw_result [i + 1 ]
304- alias = score_alias or "__score"
305325 row = {alias : score }
306326 rows .append (row )
307327 elif with_scores :
@@ -310,9 +330,6 @@ async def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
310330 score = raw_result [i + 1 ]
311331 row_data = raw_result [i + 2 ]
312332 row = dict (zip (row_data [::2 ], row_data [1 ::2 ]))
313- alias = score_alias or "__score"
314- if alias in row :
315- alias = f"__score_{ alias } "
316333 row [alias ] = score
317334 rows .append (row )
318335 else :
0 commit comments