diff options
Diffstat (limited to 'lib/sqlalchemy/sql')
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 32 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/default_comparator.py | 42 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/expression.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/operators.py | 109 | 
4 files changed, 187 insertions, 1 deletions
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 7b917e661..ec1a57935 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2109,6 +2109,24 @@ class SQLCompiler(Compiled):              **kw          ) +    def visit_regexp_match_op_binary(self, binary, operator, **kw): +        raise exc.CompileError( +            "%s dialect does not support regular expressions" +            % self.dialect.name +        ) + +    def visit_not_regexp_match_op_binary(self, binary, operator, **kw): +        raise exc.CompileError( +            "%s dialect does not support regular expressions" +            % self.dialect.name +        ) + +    def visit_regexp_replace_op_binary(self, binary, operator, **kw): +        raise exc.CompileError( +            "%s dialect does not support regular expression replacements" +            % self.dialect.name +        ) +      def visit_bindparam(          self,          bindparam, @@ -3671,6 +3689,20 @@ class StrSQLCompiler(SQLCompiler):      def get_from_hint_text(self, table, text):          return "[%s]" % text +    def visit_regexp_match_op_binary(self, binary, operator, **kw): +        return self._generate_generic_binary(binary, " <regexp> ", **kw) + +    def visit_not_regexp_match_op_binary(self, binary, operator, **kw): +        return self._generate_generic_binary(binary, " <not regexp> ", **kw) + +    def visit_regexp_replace_op_binary(self, binary, operator, **kw): +        replacement = binary.modifiers["replacement"] +        return "<regexp replace>(%s, %s, %s)" % ( +            binary.left._compiler_dispatch(self, **kw), +            binary.right._compiler_dispatch(self, **kw), +            replacement._compiler_dispatch(self, **kw), +        ) +  class DDLCompiler(Compiled):      @util.memoized_property diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py index 6f1a25670..eec174e8b 100644 --- a/lib/sqlalchemy/sql/default_comparator.py +++ b/lib/sqlalchemy/sql/default_comparator.py @@ -252,6 +252,45 @@ def _collate_impl(expr, op, other, **kw):      return collate(expr, other) +def _regexp_match_impl(expr, op, pattern, flags, **kw): +    if flags is not None: +        flags = coercions.expect( +            roles.BinaryElementRole, +            flags, +            expr=expr, +            operator=operators.regexp_replace_op, +        ) +    return _boolean_compare( +        expr, +        op, +        pattern, +        flags=flags, +        negate=operators.not_regexp_match_op +        if op is operators.regexp_match_op +        else operators.regexp_match_op, +        **kw +    ) + + +def _regexp_replace_impl(expr, op, pattern, replacement, flags, **kw): +    replacement = coercions.expect( +        roles.BinaryElementRole, +        replacement, +        expr=expr, +        operator=operators.regexp_replace_op, +    ) +    if flags is not None: +        flags = coercions.expect( +            roles.BinaryElementRole, +            flags, +            expr=expr, +            operator=operators.regexp_replace_op, +        ) +    return _binary_operate( +        expr, op, pattern, replacement=replacement, flags=flags, **kw +    ) + +  # a mapping of operators with the method they use, along with  # their negated operator for comparison operators  operator_lookup = { @@ -304,4 +343,7 @@ operator_lookup = {      "lshift": (_unsupported_impl,),      "rshift": (_unsupported_impl,),      "contains": (_unsupported_impl,), +    "regexp_match_op": (_regexp_match_impl,), +    "not_regexp_match_op": (_regexp_match_impl,), +    "regexp_replace_op": (_regexp_replace_impl,),  } diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index d60c63363..31584f072 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -44,6 +44,7 @@ __all__ = [      "case",      "cast",      "column", +    "custom_op",      "cte",      "delete",      "desc", @@ -81,6 +82,7 @@ __all__ = [      "union",      "union_all",      "update", +    "quoted_name",      "within_group",      "Subquery",      "TableSample", @@ -141,6 +143,9 @@ from .functions import modifier  # noqa  from .lambdas import lambda_stmt  # noqa  from .lambdas import LambdaElement  # noqa  from .lambdas import StatementLambdaElement  # noqa +from .operators import ColumnOperators  # noqa +from .operators import custom_op  # noqa +from .operators import Operators  # noqa  from .selectable import Alias  # noqa  from .selectable import AliasedReturnsRows  # noqa  from .selectable import CompoundSelect  # noqa diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index bb773e281..91a0792c3 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -913,7 +913,7 @@ class ColumnOperators(Operators):      def match(self, other, **kwargs):          """Implements a database-specific 'match' operator. -        :meth:`~.ColumnOperators.match` attempts to resolve to +        :meth:`_sql.ColumnOperators.match` attempts to resolve to          a MATCH-like function or operator provided by the backend.          Examples include: @@ -928,6 +928,96 @@ class ColumnOperators(Operators):          """          return self.operate(match_op, other, **kwargs) +    def regexp_match(self, pattern, flags=None): +        """Implements a database-specific 'regexp match' operator. + +        E.g.:: + +            stmt = select(table.c.some_column).where( +                table.c.some_column.regexp_match('^(b|c)') +            ) + +        :meth:`_sql.ColumnOperators.regexp_match` attempts to resolve to +        a REGEXP-like function or operator provided by the backend, however +        the specific regular expression syntax and flags available are +        **not backend agnostic**. + +        Examples include: + +        * PostgreSQL - renders ``x ~ y`` or ``x !~ y`` when negated. +        * Oracle - renders ``REGEXP_LIKE(x, y)`` +        * SQLite - uses SQLite's ``REGEXP`` placeholder operator and calls into +          the Python ``re.match()`` builtin. +        * other backends may provide special implementations. +        * Backends without any special implementation will emit +          the operator as "REGEXP" or "NOT REGEXP".  This is compatible with +          SQLite and MySQL, for example. + +        Regular expression support is currently implemented for Oracle, +        PostgreSQL, MySQL and MariaDB.  Partial support is available for +        SQLite.  Support among third-party dialects may vary. + +        :param pattern: The regular expression pattern string or column +          clause. +        :param flags: Any regular expression string flags to apply. Flags +          tend to be backend specific. It can be a string or a column clause. +          Some backends, like PostgreSQL and MariaDB, may alternatively +          specify the flags as part of the pattern. +          When using the ignore case flag 'i' in PostgreSQL, the ignore case +          regexp match operator ``~*`` or ``!~*`` will be used. + +        .. versionadded:: 1.4 + +        .. seealso:: + +            :meth:`_sql.ColumnOperators.regexp_replace` + + +        """ +        return self.operate(regexp_match_op, pattern, flags=flags) + +    def regexp_replace(self, pattern, replacement, flags=None): +        """Implements a database-specific 'regexp replace' operator. + +        E.g.:: + +            stmt = select( +                table.c.some_column.regexp_replace( +                    'b(..)', +                    'X\1Y', +                    flags='g' +                ) +            ) + +        :meth:`_sql.ColumnOperators.regexp_replace` attempts to resolve to +        a REGEXP_REPLACE-like function provided by the backend, that +        usually emit the function ``REGEXP_REPLACE()``.  However, +        the specific regular expression syntax and flags available are +        **not backend agnostic**. + +        Regular expression replacement support is currently implemented for +        Oracle, PostgreSQL, MySQL 8 or greater and MariaDB.  Support among +        third-party dialects may vary. + +        :param pattern: The regular expression pattern string or column +          clause. +        :param pattern: The replacement string or column clause. +        :param flags: Any regular expression string flags to apply. Flags +          tend to be backend specific. It can be a string or a column clause. +          Some backends, like PostgreSQL and MariaDB, may alternatively +          specify the flags as part of the pattern. + +        .. versionadded:: 1.4 + +        .. seealso:: + +            :meth:`_sql.ColumnOperators.regexp_match` + +        """ +        return self.operate( +            regexp_replace_op, pattern, replacement=replacement, flags=flags +        ) +      def desc(self):          """Produce a :func:`_expression.desc` clause against the          parent object.""" @@ -1299,6 +1389,20 @@ def match_op(a, b, **kw):  @comparison_op +def regexp_match_op(a, b, flags=None): +    return a.regexp_match(b, flags=flags) + + +@comparison_op +def not_regexp_match_op(a, b, flags=None): +    return ~a.regexp_match(b, flags=flags) + + +def regexp_replace_op(a, b, replacement, flags=None): +    return a.regexp_replace(b, replacement=replacement, flags=flags) + + +@comparison_op  def notmatch_op(a, b, **kw):      return a.notmatch(b, **kw) @@ -1417,6 +1521,9 @@ _PRECEDENCE = {      filter_op: 6,      match_op: 5,      notmatch_op: 5, +    regexp_match_op: 5, +    not_regexp_match_op: 5, +    regexp_replace_op: 5,      ilike_op: 5,      notilike_op: 5,      like_op: 5,  | 
