1# dialects/mysql/expression.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8from __future__ import annotations
9
10from typing import Any
11
12from ... import exc
13from ... import util
14from ...sql import coercions
15from ...sql import elements
16from ...sql import operators
17from ...sql import roles
18from ...sql.base import _generative
19from ...sql.base import Generative
20from ...util.typing import Self
21
22
23class match(Generative, elements.BinaryExpression[Any]):
24 """Produce a ``MATCH (X, Y) AGAINST ('TEXT')`` clause.
25
26 E.g.::
27
28 from sqlalchemy import desc
29 from sqlalchemy.dialects.mysql import match
30
31 match_expr = match(
32 users_table.c.firstname,
33 users_table.c.lastname,
34 against="Firstname Lastname",
35 )
36
37 stmt = (
38 select(users_table)
39 .where(match_expr.in_boolean_mode())
40 .order_by(desc(match_expr))
41 )
42
43 Would produce SQL resembling:
44
45 .. sourcecode:: sql
46
47 SELECT id, firstname, lastname
48 FROM user
49 WHERE MATCH(firstname, lastname) AGAINST (:param_1 IN BOOLEAN MODE)
50 ORDER BY MATCH(firstname, lastname) AGAINST (:param_2) DESC
51
52 The :func:`_mysql.match` function is a standalone version of the
53 :meth:`_sql.ColumnElement.match` method available on all
54 SQL expressions, as when :meth:`_expression.ColumnElement.match` is
55 used, but allows to pass multiple columns
56
57 :param cols: column expressions to match against
58
59 :param against: expression to be compared towards
60
61 :param in_boolean_mode: boolean, set "boolean mode" to true
62
63 :param in_natural_language_mode: boolean , set "natural language" to true
64
65 :param with_query_expansion: boolean, set "query expansion" to true
66
67 .. versionadded:: 1.4.19
68
69 .. seealso::
70
71 :meth:`_expression.ColumnElement.match`
72
73 """
74
75 __visit_name__ = "mysql_match"
76
77 inherit_cache = True
78 modifiers: util.immutabledict[str, Any]
79
80 def __init__(self, *cols: elements.ColumnElement[Any], **kw: Any):
81 if not cols:
82 raise exc.ArgumentError("columns are required")
83
84 against = kw.pop("against", None)
85
86 if against is None:
87 raise exc.ArgumentError("against is required")
88 against = coercions.expect(
89 roles.ExpressionElementRole,
90 against,
91 )
92
93 left = elements.BooleanClauseList._construct_raw(
94 operators.comma_op,
95 clauses=cols,
96 )
97 left.group = False
98
99 flags = util.immutabledict(
100 {
101 "mysql_boolean_mode": kw.pop("in_boolean_mode", False),
102 "mysql_natural_language": kw.pop(
103 "in_natural_language_mode", False
104 ),
105 "mysql_query_expansion": kw.pop("with_query_expansion", False),
106 }
107 )
108
109 if kw:
110 raise exc.ArgumentError("unknown arguments: %s" % (", ".join(kw)))
111
112 super().__init__(left, against, operators.match_op, modifiers=flags)
113
114 @_generative
115 def in_boolean_mode(self) -> Self:
116 """Apply the "IN BOOLEAN MODE" modifier to the MATCH expression.
117
118 :return: a new :class:`_mysql.match` instance with modifications
119 applied.
120 """
121
122 self.modifiers = self.modifiers.union({"mysql_boolean_mode": True})
123 return self
124
125 @_generative
126 def in_natural_language_mode(self) -> Self:
127 """Apply the "IN NATURAL LANGUAGE MODE" modifier to the MATCH
128 expression.
129
130 :return: a new :class:`_mysql.match` instance with modifications
131 applied.
132 """
133
134 self.modifiers = self.modifiers.union({"mysql_natural_language": True})
135 return self
136
137 @_generative
138 def with_query_expansion(self) -> Self:
139 """Apply the "WITH QUERY EXPANSION" modifier to the MATCH expression.
140
141 :return: a new :class:`_mysql.match` instance with modifications
142 applied.
143 """
144
145 self.modifiers = self.modifiers.union({"mysql_query_expansion": True})
146 return self