Repository: django/django
File: django/contrib/postgres/search.py (Python)

Code before:
)
from django.db.models.expressions import CombinedExpression, register_combinable_fields
from django.db.models.functions import Cast, Coalesce

from .utils import CheckPostgresInstalledMixin


class SearchVectorExact(Lookup):
    lookup_name = "exact"

    def process_rhs(self, qn, connection):
        if not isinstance(self.rhs, (SearchQuery, CombinedSearchQuery)):
            config = getattr(self.lhs, "config", None)
            self.rhs = SearchQuery(self.rhs, config=config)
        rhs, rhs_params = super().process_rhs(qn, connection)
        return rhs, rhs_params

    def as_sql(self, qn, connection):
        lhs, lhs_params = self.process_lhs(qn, connection)
        rhs, rhs_params = self.process_rhs(qn, connection)
        params = (*lhs_params, *rhs_params)
        return "%s @@ %s" % (lhs, rhs), params


class SearchVectorField(CheckPostgresInstalledMixin, Field):
    def db_type(self, connection):
        return "tsvector"


class SearchQueryField(CheckPostgresInstalledMixin, Field):
    def db_type(self, connection):
        return "tsquery"


class _Float4Field(Field):
    def db_type(self, connection):
        return "float4"


class SearchConfig(Expression):
    def __init__(self, config):
        super().__init__()
        if not hasattr(config, "resolve_expression"):
            config = Value(config)
        self.config = config

    @classmethod
    def from_parameter(cls, config):
        if config is None or isinstance(config, cls):
            return config
        return cls(config)

Review comment (suggestion):
```suggestion
    """Replace unsafe chars with space and convert multiple spaces to single."""
    return normalize_spaces(_spec_chars_re.sub(" ", query))
```


Code after:
    Value,
)
from django.db.models.expressions import CombinedExpression, register_combinable_fields
from django.db.models.functions import Cast, Coalesce
from django.utils.regex_helper import _lazy_re_compile

from .utils import CheckPostgresInstalledMixin

if is_psycopg3:
    from psycopg.adapt import Dumper

    class UTF8Dumper(Dumper):
        def dump(self, obj):
            return bytes(obj, "utf-8")

    def quote_lexeme(value):
        return UTF8Dumper(str).quote(psql_escape(value)).decode()

else:
    from psycopg2.extensions import adapt

    def quote_lexeme(value):
        adapter = adapt(psql_escape(value))
        adapter.encoding = "utf-8"
        return adapter.getquoted().decode()


spec_chars_re = _lazy_re_compile(r"['\0\[\]()|&:*!@<>\\]")
multiple_spaces_re = _lazy_re_compile(r"\s{2,}")


def normalize_spaces(val):
    """Convert multiple spaces to single and strip from both sides."""
    if not (val := val.strip()):
        return None
    return multiple_spaces_re.sub(" ", val)


def psql_escape(query):
    """Replace chars not fit for use in search queries with a single space."""
    query = spec_chars_re.sub(" ", query)
    return normalize_spaces(query)


class SearchVectorExact(Lookup):
    lookup_name = "exact"

    def process_rhs(self, qn, connection):
        if not isinstance(self.rhs, (SearchQuery, CombinedSearchQuery)):
            config = getattr(self.lhs, "config", None)
            self.rhs = SearchQuery(self.rhs, config=config)