utils.py 8.88 KB
Newer Older
1
# copyright 2004-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of rql.
#
# rql is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# rql is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with rql. If not, see <http://www.gnu.org/licenses/>.
18
"""Miscellaneous utilities for RQL."""
Nicolas Chauvat's avatar
Nicolas Chauvat committed
19

Nsukami Patrick's avatar
Nsukami Patrick committed
20
21
import string

22
23
from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr, CAST
from logilab.common.decorators import monkeypatch
Nicolas Chauvat's avatar
Nicolas Chauvat committed
24

25
from rql._exceptions import BadRQLQuery
Nsukami Patrick's avatar
Nsukami Patrick committed
26
from typing import (
27
28
29
30
31
32
33
34
35
    TYPE_CHECKING,
    Set,
    Optional,
    Mapping,
    Any,
    Callable,
    Generator,
    List,
    Union as Union_,
Nsukami Patrick's avatar
Nsukami Patrick committed
36
)
37

Nsukami Patrick's avatar
Nsukami Patrick committed
38
39
40
if TYPE_CHECKING:
    import rql
    import logilab
41

42
43
    from rql.base import BaseNode

Nsukami Patrick's avatar
Nsukami Patrick committed
44
__docformat__: str = "restructuredtext en"
45
46


Nsukami Patrick's avatar
Nsukami Patrick committed
47
def decompose_b26(index: int, table: str = string.ascii_uppercase) -> str:
48
    """Return a letter (base-26) decomposition of index."""
49
    div, mod = divmod(index, 26)
Nsukami Patrick's avatar
Nsukami Patrick committed
50

51
52
    if div == 0:
        return table[mod]
Nsukami Patrick's avatar
Nsukami Patrick committed
53
54

    return decompose_b26(div - 1) + table[mod]
55

56

Nsukami Patrick's avatar
Nsukami Patrick committed
57
class rqlvar_maker:
58
    """Yields consistent RQL variable names.
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
59

60
61
62
63
    :param stop: optional argument to stop iteration after the Nth variable
                 default is None which means 'never stop'
    :param defined: optional dict of already defined vars
    """
Nsukami Patrick's avatar
Nsukami Patrick committed
64

65
66
    # NOTE: written a an iterator class instead of a simple generator to be
    #       picklable
67

Nsukami Patrick's avatar
Nsukami Patrick committed
68
69
70
71
72
73
74
    def __init__(
        self,
        stop: Optional[int] = None,
        index: int = 0,
        defined: Optional[Mapping[str, Any]] = None,
        aliases: Optional[Mapping[str, Any]] = None,
    ) -> None:
75
76
77
        self.index = index
        self.stop = stop
        self.defined = defined
78
        self.aliases = aliases
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
79

80
81
    def __iter__(self):
        return self
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
82

Nsukami Patrick's avatar
Nsukami Patrick committed
83
    def __next__(self) -> str:
84
85
86
87
88
        while self.stop is None or self.index < self.stop:
            var = decompose_b26(self.index)
            self.index += 1
            if self.defined is not None and var in self.defined:
                continue
89
90
            if self.aliases is not None and var in self.aliases:
                continue
91
92
            return var
        raise StopIteration()
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
93

Rémi Cardona's avatar
Rémi Cardona committed
94
95
96
    next = __next__


Nsukami Patrick's avatar
Nsukami Patrick committed
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
KEYWORDS: Set[str] = set(
    (
        "INSERT",
        "SET",
        "DELETE",
        "UNION",
        "WITH",
        "BEING",
        "WHERE",
        "AND",
        "OR",
        "NOT",
        "IN",
        "LIKE",
        "ILIKE",
        "EXISTS",
        "DISTINCT",
        "TRUE",
        "FALSE",
        "NULL",
        "TODAY",
        "GROUPBY",
        "HAVING",
        "ORDERBY",
        "ASC",
        "DESC",
        "LIMIT",
        "OFFSET",
    )
)

RQL_FUNCTIONS_REGISTRY: "logilab.database._FunctionRegistry" = (
    SQL_FUNCTIONS_REGISTRY.copy()
)
131

132

Simon Chabot's avatar
Simon Chabot committed
133
@monkeypatch(FunctionDescr)  # type: ignore[no-redef]
Laurent Peuch's avatar
Laurent Peuch committed
134
def st_description(  # noqa
Nsukami Patrick's avatar
Nsukami Patrick committed
135
136
137
138
139
140
    self,
    funcnode: "rql.nodes.Function",
    mainindex: Optional[int],
    tr: Callable[[str], str],
) -> str:
    return "%s(%s)" % (
141
        tr(self.name),
Nsukami Patrick's avatar
Nsukami Patrick committed
142
143
144
145
146
147
148
        ", ".join(
            sorted(
                child.get_description(mainindex, tr)
                for child in iter_funcnode_variables(funcnode)
            )
        ),
    )
Sylvain's avatar
Sylvain committed
149

150

151
@monkeypatch(FunctionDescr)
Nsukami Patrick's avatar
Nsukami Patrick committed
152
153
def st_check_backend(self, backend: Any, funcnode: "rql.nodes.Function") -> None:
    # XXX backend seems to always be None
154
    if not self.supports(backend):
Nsukami Patrick's avatar
Nsukami Patrick committed
155
156
157
        raise BadRQLQuery(
            "backend %s doesn't support function %s" % (backend, self.name)
        )
158
159


160
@monkeypatch(FunctionDescr)  # type: ignore[no-redef]
161
def rql_return_type(self, funcnode: "rql.nodes.Function") -> Optional[str]:
162
163
    return self.rtype

164

165
166
@monkeypatch(CAST)  # type: ignore[no-redef] # noqa: F811
def st_description(  # noqa: F811
Nsukami Patrick's avatar
Nsukami Patrick committed
167
168
169
170
171
    self,
    funcnode: "rql.nodes.Function",
    mainindex: Optional[int],
    tr: Callable[[str], str],
) -> str:
172
173
    return self.rql_return_type(funcnode)

174

175
176
@monkeypatch(CAST)  # type: ignore[no-redef] # noqa: F811
def rql_return_type(self, funcnode: "rql.nodes.Function") -> str:  # noqa: F811
177
178
179
180
181
    # mypy: "BaseNode" has no attribute "value"  [attr-defined]
    # this is black magic to set rql_return_type to logilab.database classes
    # this code assume that CAST only works with nodes that has a value
    # attribute like Constant
    return funcnode.children[0].value  # type: ignore[attr-defined]
182
183


Nsukami Patrick's avatar
Nsukami Patrick committed
184
185
186
def iter_funcnode_variables(funcnode: "rql.nodes.Function") -> Generator:
    # funcnode: rql.nodes.Function
    # term: rql.nodes.VariableRef
187
188
    for term in funcnode.children:
        try:
Nsukami Patrick's avatar
Nsukami Patrick committed
189
            # term.variable.stinfo: dict
190
191
            # mypy: "BaseNode" has no attribute "variable"  [attr-defined]
            # same black magic than in rql_return_type above
192
193
194
195

            # error: Item "None" of "Optional[Variable]" has no attribute "stinfo"  [union-attr]
            assert getattr(term, "variable") is not None

196
            yield term.variable.stinfo["attrvar"] or term  # type: ignore[attr-defined]
197
        except AttributeError:
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
198
            yield term
Nicolas Chauvat's avatar
Nicolas Chauvat committed
199

200

Nsukami Patrick's avatar
Nsukami Patrick committed
201
def is_keyword(word: str) -> bool:
202
    """Return true if the given word is a RQL keyword."""
203
    return word.upper() in KEYWORDS
Nicolas Chauvat's avatar
Nicolas Chauvat committed
204

205

Nsukami Patrick's avatar
Nsukami Patrick committed
206
def common_parent(
207
    node1: Optional["BaseNode"], node2: Optional["BaseNode"]
208
) -> "BaseNode":
209
210
211
212
213
214
215
    """return the first common parent between node1 and node2

    algorithm :
     1) index node1's parents
     2) climb among node2's parents until we find a common parent
    """
    # index node1's parents
216
    node1_parents: Set["BaseNode"] = set()
217
218
219
220
221
222
223
224
    while node1:
        node1_parents.add(node1)
        node1 = node1.parent
    # climb among node2's parents until we find a common parent
    while node2:
        if node2 in node1_parents:
            return node2
        node2 = node2.parent
Nsukami Patrick's avatar
Nsukami Patrick committed
225
    raise Exception(f"Failed to get a common parent between '{node1}' and '{node2}'")
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
226

227

Nsukami Patrick's avatar
Nsukami Patrick committed
228
def register_function(funcdef: "logilab.database.FunctionDescr") -> None:
229
230
    RQL_FUNCTIONS_REGISTRY.register_function(funcdef)
    SQL_FUNCTIONS_REGISTRY.register_function(funcdef)
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
231

232

Nsukami Patrick's avatar
Nsukami Patrick committed
233
234
def function_description(funcname: str) -> "logilab.database.FunctionDescr":
    # return type is 'logilab.database.' + funcname
Sylvain Thénault's avatar
Sylvain Thénault committed
235
    """Return the description (:class:`FunctionDescr`) for a RQL function."""
236
    return RQL_FUNCTIONS_REGISTRY.get_function(funcname)
Nicolas Chauvat's avatar
Nicolas Chauvat committed
237

238

Nsukami Patrick's avatar
Nsukami Patrick committed
239
def quote(value: str) -> str:
240
    """Quote a string value."""
Nicolas Chauvat's avatar
Nicolas Chauvat committed
241
242
243
    res = ['"']
    for char in value:
        if char == '"':
Nsukami Patrick's avatar
Nsukami Patrick committed
244
            res.append("\\")
Nicolas Chauvat's avatar
Nicolas Chauvat committed
245
246
        res.append(char)
    res.append('"')
Nsukami Patrick's avatar
Nsukami Patrick committed
247
    return "".join(res)
Nicolas Chauvat's avatar
Nicolas Chauvat committed
248

249

Nsukami Patrick's avatar
Nsukami Patrick committed
250
251
def uquote(value: str) -> str:
    # XXX is this still useful?
252
    """Quote a unicode string value."""
Nsukami Patrick's avatar
Nsukami Patrick committed
253
    res: List[str] = ['"']
Nicolas Chauvat's avatar
Nicolas Chauvat committed
254
    for char in value:
255
256
        if char == '"':
            res.append("\\")
Nicolas Chauvat's avatar
Nicolas Chauvat committed
257
        res.append(char)
258
259
    res.append('"')
    return "".join(res)
Nicolas Chauvat's avatar
Nicolas Chauvat committed
260

261

262
class VisitableMixIn:
263
264
265
266
267
268
    def accept(
        self,
        visitor: Union_["rql.stcheck.RQLSTChecker", "rql.stcheck.RQLSTAnnotator"],
        *args: Optional["rql.stcheck.STCheckState"],
        **kwargs: Optional[Any],
    ) -> None:
Nsukami Patrick's avatar
Nsukami Patrick committed
269
        visit_id: str = self.__class__.__name__.lower()
270
        visit_method: Callable = getattr(visitor, "visit_%s" % visit_id)
271
272
        return visit_method(self, *args, **kwargs)

273
274
275
276
277
278
    def leave(
        self,
        visitor: Union_["rql.stcheck.RQLSTChecker", "rql.stcheck.RQLSTAnnotator"],
        *args: Optional["rql.stcheck.STCheckState"],
        **kwargs: Optional[Any],
    ) -> None:
Nsukami Patrick's avatar
Nsukami Patrick committed
279
        visit_id: str = self.__class__.__name__.lower()
280
        visit_method: Callable = getattr(visitor, "leave_%s" % visit_id)
281
        return visit_method(self, *args, **kwargs)
282

283

Nsukami Patrick's avatar
Nsukami Patrick committed
284
# should we redefine this as a Protocol?
285
class RQLVisitorHandler:
286
287
    """Handler providing a dummy implementation of all callbacks necessary
    to visit a RQL syntax tree.
Nicolas Chauvat's avatar
Nicolas Chauvat committed
288
    """
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
289

290
    def visit_union(self, union):
Nicolas Chauvat's avatar
Nicolas Chauvat committed
291
        pass
292

Nicolas Chauvat's avatar
Nicolas Chauvat committed
293
294
    def visit_insert(self, insert):
        pass
295

Nicolas Chauvat's avatar
Nicolas Chauvat committed
296
297
    def visit_delete(self, delete):
        pass
298

299
    def visit_set(self, update):
Nicolas Chauvat's avatar
Nicolas Chauvat committed
300
        pass
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
301

302
    def visit_select(self, selection):
Nicolas Chauvat's avatar
Nicolas Chauvat committed
303
        pass
304

Nicolas Chauvat's avatar
Nicolas Chauvat committed
305
306
    def visit_sortterm(self, sortterm):
        pass
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
307

Nicolas Chauvat's avatar
Nicolas Chauvat committed
308
309
    def visit_and(self, et):
        pass
310

Nicolas Chauvat's avatar
Nicolas Chauvat committed
311
    def visit_or(self, ou):
Sylvain Thénault's avatar
d-t-w    
Sylvain Thénault committed
312
        pass
313

Sylvain Thenault's avatar
Sylvain Thenault committed
314
315
    def visit_not(self, not_):
        pass
316

Nicolas Chauvat's avatar
Nicolas Chauvat committed
317
318
    def visit_relation(self, relation):
        pass
319

Nicolas Chauvat's avatar
Nicolas Chauvat committed
320
321
    def visit_comparison(self, comparison):
        pass
322

Nicolas Chauvat's avatar
Nicolas Chauvat committed
323
324
    def visit_mathexpression(self, mathexpression):
        pass
325

Nicolas Chauvat's avatar
Nicolas Chauvat committed
326
327
    def visit_function(self, function):
        pass
328

Nicolas Chauvat's avatar
Nicolas Chauvat committed
329
330
    def visit_variableref(self, variable):
        pass
331

Nicolas Chauvat's avatar
Nicolas Chauvat committed
332
333
    def visit_constant(self, constant):
        pass