diff --git a/paradedb/sqlalchemy/alembic.py b/paradedb/sqlalchemy/alembic.py index ce722bc..34dd957 100644 --- a/paradedb/sqlalchemy/alembic.py +++ b/paradedb/sqlalchemy/alembic.py @@ -1,7 +1,5 @@ from __future__ import annotations -import re - from alembic.autogenerate import comparators, renderers from alembic.operations import Operations from alembic.operations.ops import MigrateOperation @@ -210,12 +208,56 @@ def _normalize_bm25_expression(expr: str) -> str: normalized = "".join(expr.split()) normalized = normalized.replace('"', "") normalized = normalized.replace("::text", "") - # Ignore schema/table qualification differences, but keep tokenizer namespaces like `pdb.simple`. - previous = None - while previous != normalized: - previous = normalized - normalized = re.sub(r"(? str: + """Strip relation qualifiers outside SQL string literals. + + Preserves tokenizer namespaces like ``pdb.simple`` and leaves quoted literal + content untouched (for example regex patterns like ``'run.*'``). + """ + out: list[str] = [] + i = 0 + in_single = False + while i < len(expr): + ch = expr[i] + + if ch == "'": + out.append(ch) + # Escaped quote inside a string literal: ''. + if in_single and i + 1 < len(expr) and expr[i + 1] == "'": + out.append("'") + i += 2 + continue + in_single = not in_single + i += 1 + continue + + if not in_single and (ch.isalpha() or ch == "_"): + j = i + 1 + while j < len(expr) and (expr[j].isalnum() or expr[j] == "_"): + j += 1 + + token = expr[i:j] + if j < len(expr) and expr[j] == ".": + if token.lower() != "pdb": + # Drop relation-like qualifier prefixes, e.g. public.products. + i = j + 1 + continue + out.append(token) + out.append(".") + i = j + 1 + continue + + out.append(token) + i = j + continue + + out.append(ch) + i += 1 + + return "".join(out) def _normalized_expression_list(expressions: list[str]) -> list[str]: diff --git a/tests/unit/test_alembic_unit.py b/tests/unit/test_alembic_unit.py index 486f0d2..f42304c 100644 --- a/tests/unit/test_alembic_unit.py +++ b/tests/unit/test_alembic_unit.py @@ -265,3 +265,15 @@ def test_suppress_standard_bm25_ops_noop_when_no_bm25(): pdb_alembic._suppress_standard_bm25_ops(upgrade_ops, set()) assert len(upgrade_ops.ops) == 1 + + +def test_normalize_bm25_expression_keeps_dotted_literal_content(): + expr = "(description)::pdb.regex_pattern('run.*')" + normalized = pdb_alembic._normalize_bm25_expression(expr) + assert normalized == "(description)::pdb.regex_pattern('run.*')" + + +def test_normalize_bm25_expression_strips_relation_qualifiers_only(): + expr = '"public"."products"."description"::pdb.simple(\'alias=description_simple\')' + normalized = pdb_alembic._normalize_bm25_expression(expr) + assert normalized == "description::pdb.simple('alias=description_simple')"