@@ -116,18 +116,28 @@ def _has_return_0(args: list[str]) -> bool:
116116 return False
117117
118118 @staticmethod
119- def _resolve_score_alias (score_alias : str | None , args : list [str ]) -> str :
119+ def _resolve_score_alias (
120+ score_alias : str | None ,
121+ args : list [str ],
122+ first_row_fields : set [str ] | None = None ,
123+ ) -> str :
120124 """Determine a stable score column name that won't collide with
121125 document fields. The alias is decided once before iterating rows
122- so every row uses the same column name."""
126+ so every row uses the same column name.
127+
128+ When a RETURN clause is present, the returned field names are used
129+ for collision detection. When RETURN is absent (SELECT *), the
130+ caller should pass ``first_row_fields`` — the field names from the
131+ first result row — so we can detect collisions even when all
132+ document attributes are returned."""
123133 alias = score_alias or "__score"
124134 # Extract RETURN field names from args to detect collision
125135 try :
126136 idx = args .index ("RETURN" )
127137 count = int (args [idx + 1 ])
128138 return_fields = set (args [idx + 2 : idx + 2 + count ])
129139 except (ValueError , IndexError ):
130- return_fields = set ()
140+ return_fields = first_row_fields or set ()
131141 if alias in return_fields :
132142 alias = f"__score_{ alias } "
133143 return alias
@@ -201,28 +211,35 @@ def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
201211 # RETURN 0 suppresses document fields (like NOCONTENT);
202212 # with WITHSCORES the reply is [count, id, score, id, score, ...]
203213 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- )
214+
215+ # Pre-resolve score alias; may be deferred for SELECT *
216+ score_alias : str | None = None
210217
211218 if with_scores and no_content :
212219 # WITHSCORES + RETURN 0: [count, id1, score1, id2, score2, ...]
213220 # Stride of 2: key, score (no field array)
221+ score_alias = self ._resolve_score_alias (
222+ translated .score_alias , translated .args
223+ )
214224 for i in range (1 , len (raw_result ) - 1 , 2 ):
215225 score = raw_result [i + 1 ]
216- row = {alias : score }
226+ row = {score_alias : score }
217227 rows .append (row )
218228 elif with_scores :
219229 # WITHSCORES format: [count, key1, score1, [fields1], key2, score2, [fields2], ...]
220230 # Stride of 3: key, score, field_list
231+ # Resolve alias using first row's fields for SELECT * (no RETURN)
221232 for i in range (1 , len (raw_result ) - 2 , 3 ):
222233 score = raw_result [i + 1 ]
223234 row_data = raw_result [i + 2 ]
224235 row = dict (zip (row_data [::2 ], row_data [1 ::2 ]))
225- row [alias ] = score
236+ if score_alias is None :
237+ score_alias = self ._resolve_score_alias (
238+ translated .score_alias ,
239+ translated .args ,
240+ first_row_fields = set (row .keys ()),
241+ )
242+ row [score_alias ] = score
226243 rows .append (row )
227244 else :
228245 # Standard format: [count, key1, [fields1], key2, [fields2], ...]
@@ -312,25 +329,31 @@ async def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
312329 if translated .command == "FT.SEARCH" :
313330 with_scores = "WITHSCORES" in translated .args
314331 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- )
332+
333+ score_alias : str | None = None
320334
321335 if with_scores and no_content :
322336 # WITHSCORES + RETURN 0: [count, id1, score1, id2, score2, ...]
337+ score_alias = self ._resolve_score_alias (
338+ translated .score_alias , translated .args
339+ )
323340 for i in range (1 , len (raw_result ) - 1 , 2 ):
324341 score = raw_result [i + 1 ]
325- row = {alias : score }
342+ row = {score_alias : score }
326343 rows .append (row )
327344 elif with_scores :
328345 # WITHSCORES format: [count, key1, score1, [fields1], ...]
329346 for i in range (1 , len (raw_result ) - 2 , 3 ):
330347 score = raw_result [i + 1 ]
331348 row_data = raw_result [i + 2 ]
332349 row = dict (zip (row_data [::2 ], row_data [1 ::2 ]))
333- row [alias ] = score
350+ if score_alias is None :
351+ score_alias = self ._resolve_score_alias (
352+ translated .score_alias ,
353+ translated .args ,
354+ first_row_fields = set (row .keys ()),
355+ )
356+ row [score_alias ] = score
334357 rows .append (row )
335358 else :
336359 # Standard format: [count, key1, [fields1], key2, [fields2], ...]
0 commit comments