@@ -33,12 +33,14 @@ def __init__(
3333 key_field : str ,
3434 * ,
3535 table_schema : str | None = None ,
36+ where : str | None = None ,
3637 ) -> None :
3738 self .index_name = index_name
3839 self .table_name = table_name
3940 self .expressions = expressions
4041 self .key_field = key_field
4142 self .table_schema = table_schema
43+ self .where = where
4244
4345 @classmethod
4446 def create_bm25_index (
@@ -50,6 +52,7 @@ def create_bm25_index(
5052 * ,
5153 key_field : str ,
5254 table_schema : str | None = None ,
55+ where : str | None = None ,
5356 ) -> MigrateOperation :
5457 return operations .invoke (
5558 cls (
@@ -58,6 +61,7 @@ def create_bm25_index(
5861 expressions ,
5962 key_field ,
6063 table_schema = table_schema ,
64+ where = where ,
6165 )
6266 )
6367
@@ -70,6 +74,8 @@ def _create_bm25_index_impl(operations: Operations, operation: CreateBM25IndexOp
7074 f"ON { _quote_qualified (operation .table_schema , operation .table_name )} "
7175 f"USING bm25 ({ expressions_sql } ) WITH (key_field={ _quote_literal (operation .key_field )} )"
7276 )
77+ if operation .where is not None :
78+ sql += f" WHERE { operation .where } "
7379 operations .execute (sql )
7480
7581
@@ -83,6 +89,8 @@ def _render_create_bm25_index_op(autogen_context, op: CreateBM25IndexOp) -> str:
8389 ]
8490 if op .table_schema is not None :
8591 parts .append (f"table_schema={ op .table_schema !r} " )
92+ if op .where is not None :
93+ parts .append (f"where={ op .where !r} " )
8694 return f"op.create_bm25_index({ ', ' .join (parts )} )"
8795
8896
@@ -164,8 +172,8 @@ def _autogen_bm25_meta_indexes(
164172
165173
166174def _autogen_bm25_db_indexes (conn , effective_schemas : set [str ]) -> dict [tuple [str , str ], dict ]:
167- """Return {(schema, index_name): {table_name, expressions, key_field}} from pg_indexes."""
168- from .indexing import _extract_bm25_field_list , _extract_key_field
175+ """Return {(schema, index_name): {table_name, expressions, key_field, where }} from pg_indexes."""
176+ from .indexing import _extract_bm25_field_list , _extract_key_field , _extract_where_clause
169177
170178 result : dict [tuple [str , str ], dict ] = {}
171179 for schema in effective_schemas :
@@ -187,6 +195,7 @@ def _autogen_bm25_db_indexes(conn, effective_schemas: set[str]) -> dict[tuple[st
187195 "table_name" : row .tablename ,
188196 "expressions" : raw_fields ,
189197 "key_field" : _extract_key_field (row .indexdef ) or "" ,
198+ "where" : _extract_where_clause (row .indexdef ),
190199 }
191200 return result
192201
@@ -264,6 +273,35 @@ def _normalized_expression_list(expressions: list[str]) -> list[str]:
264273 return [_normalize_bm25_expression (expr ) for expr in expressions ]
265274
266275
276+ def _normalize_where (clause : str | None ) -> str | None :
277+ """Normalize a WHERE clause string for comparison.
278+
279+ Strips whitespace, removes double quotes, and lowercases to reduce
280+ false-positive drift detection between PostgreSQL's normalized form
281+ and the SQLAlchemy-compiled form.
282+ """
283+ if clause is None :
284+ return None
285+ normalized = " " .join (clause .split ())
286+ normalized = normalized .replace ('"' , "" )
287+ return normalized .lower ()
288+
289+
290+ def _render_where_from_index (index ) -> str | None :
291+ """Compile the ``postgresql_where`` clause from a SQLAlchemy Index to SQL text."""
292+ where_clause = index .dialect_options ["postgresql" ].get ("where" )
293+ if where_clause is None :
294+ return None
295+ if isinstance (where_clause , ClauseElement ):
296+ return str (
297+ where_clause .compile (
298+ dialect = postgresql .dialect (),
299+ compile_kwargs = {"literal_binds" : True },
300+ )
301+ )
302+ return str (where_clause )
303+
304+
267305def _suppress_standard_bm25_ops (upgrade_ops , bm25_names : set [str ]) -> None :
268306 """Remove any standard Alembic CreateIndexOp/DropIndexOp for BM25 indexes."""
269307 from alembic .operations .ops import CreateIndexOp , DropIndexOp , ModifyTableOps
@@ -317,38 +355,36 @@ def _compare_bm25_indexes(autogen_context, upgrade_ops, schemas) -> PriorityDisp
317355 upgrade_ops .ops .append (DropBM25IndexOp (index_name = key [1 ], if_exists = True , schema = key [0 ]))
318356
319357 # Emit create ops for indexes present in MetaData but absent from DB.
320- # Also re-create indexes whose expression list or key_field differs from the DB.
358+ # Also re-create indexes whose expression list, key_field, or WHERE clause differs from the DB.
321359 for key , index in meta_bm25 .items ():
322360 with_opts = index .dialect_options ["postgresql" ].get ("with" ) or {}
323361 key_field = with_opts .get ("key_field" , "" )
324362 expressions = [
325363 _strip_relation_qualifiers (_render_bm25_expression (expr ), index .table .name )
326364 for expr in index .expressions
327365 ]
366+ meta_where = _render_where_from_index (index )
367+
368+ def _make_create_op ():
369+ return CreateBM25IndexOp (
370+ index_name = index .name ,
371+ table_name = index .table .name ,
372+ expressions = expressions ,
373+ key_field = key_field ,
374+ table_schema = key [0 ],
375+ where = meta_where ,
376+ )
328377
329378 if key not in db_bm25 :
330- upgrade_ops .ops .append (
331- CreateBM25IndexOp (
332- index_name = index .name ,
333- table_name = index .table .name ,
334- expressions = expressions ,
335- key_field = key_field ,
336- table_schema = key [0 ],
337- )
338- )
379+ upgrade_ops .ops .append (_make_create_op ())
339380 else :
340381 db = db_bm25 [key ]
341- if _normalized_expression_list (db ["expressions" ]) != _normalized_expression_list (expressions ) or db ["key_field" ] != key_field :
382+ expressions_changed = _normalized_expression_list (db ["expressions" ]) != _normalized_expression_list (expressions )
383+ key_field_changed = db ["key_field" ] != key_field
384+ where_changed = _normalize_where (db .get ("where" )) != _normalize_where (meta_where )
385+ if expressions_changed or key_field_changed or where_changed :
342386 # Index configuration changed: drop the old one, create the new one.
343387 upgrade_ops .ops .append (DropBM25IndexOp (index_name = key [1 ], if_exists = True , schema = key [0 ]))
344- upgrade_ops .ops .append (
345- CreateBM25IndexOp (
346- index_name = index .name ,
347- table_name = index .table .name ,
348- expressions = expressions ,
349- key_field = key_field ,
350- table_schema = key [0 ],
351- )
352- )
388+ upgrade_ops .ops .append (_make_create_op ())
353389
354390 return PriorityDispatchResult .CONTINUE
0 commit comments