Skip to content

Commit b503cfb

Browse files
ES|QL query builder (elastic#2997)
* ES|QL query builder * add missing esql api documentation * add FORK command * initial attempt at generating all functions * unit tests * more operators * documentation * integration tests * add new COMPLETION command * show ES|QL in all docs examples * Docstring fixes * add technical preview warning
1 parent 218565c commit b503cfb

File tree

10 files changed

+4041
-18
lines changed

10 files changed

+4041
-18
lines changed

docs/sphinx/esql.rst

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
ES|QL Query Builder
2+
===================
3+
4+
Commands
5+
--------
6+
7+
.. autoclass:: elasticsearch.esql.ESQL
8+
:inherited-members:
9+
:members:
10+
11+
.. autoclass:: elasticsearch.esql.esql.ESQLBase
12+
:inherited-members:
13+
:members:
14+
:exclude-members: __init__
15+
16+
.. autoclass:: elasticsearch.esql.esql.From
17+
:members:
18+
:exclude-members: __init__
19+
20+
.. autoclass:: elasticsearch.esql.esql.Row
21+
:members:
22+
:exclude-members: __init__
23+
24+
.. autoclass:: elasticsearch.esql.esql.Show
25+
:members:
26+
:exclude-members: __init__
27+
28+
.. autoclass:: elasticsearch.esql.esql.ChangePoint
29+
:members:
30+
:exclude-members: __init__
31+
32+
.. autoclass:: elasticsearch.esql.esql.Completion
33+
:members:
34+
:exclude-members: __init__
35+
36+
.. autoclass:: elasticsearch.esql.esql.Dissect
37+
:members:
38+
:exclude-members: __init__
39+
40+
.. autoclass:: elasticsearch.esql.esql.Drop
41+
:members:
42+
:exclude-members: __init__
43+
44+
.. autoclass:: elasticsearch.esql.esql.Enrich
45+
:members:
46+
:exclude-members: __init__
47+
48+
.. autoclass:: elasticsearch.esql.esql.Eval
49+
:members:
50+
:exclude-members: __init__
51+
52+
.. autoclass:: elasticsearch.esql.esql.Fork
53+
:members:
54+
:exclude-members: __init__
55+
56+
.. autoclass:: elasticsearch.esql.esql.Grok
57+
:members:
58+
:exclude-members: __init__
59+
60+
.. autoclass:: elasticsearch.esql.esql.Keep
61+
:members:
62+
:exclude-members: __init__
63+
64+
.. autoclass:: elasticsearch.esql.esql.Limit
65+
:members:
66+
:exclude-members: __init__
67+
68+
.. autoclass:: elasticsearch.esql.esql.LookupJoin
69+
:members:
70+
:exclude-members: __init__
71+
72+
.. autoclass:: elasticsearch.esql.esql.MvExpand
73+
:members:
74+
:exclude-members: __init__
75+
76+
.. autoclass:: elasticsearch.esql.esql.Rename
77+
:members:
78+
:exclude-members: __init__
79+
80+
.. autoclass:: elasticsearch.esql.esql.Sample
81+
:members:
82+
:exclude-members: __init__
83+
84+
.. autoclass:: elasticsearch.esql.esql.Sort
85+
:members:
86+
:exclude-members: __init__
87+
88+
.. autoclass:: elasticsearch.esql.esql.Stats
89+
:members:
90+
:exclude-members: __init__
91+
92+
.. autoclass:: elasticsearch.esql.esql.Where
93+
:members:
94+
:exclude-members: __init__
95+
96+
Functions
97+
---------
98+
99+
.. automodule:: elasticsearch.esql.functions
100+
:members:

elasticsearch/dsl/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .aggs import A, Agg
2020
from .analysis import analyzer, char_filter, normalizer, token_filter, tokenizer
2121
from .document import AsyncDocument, Document
22-
from .document_base import InnerDoc, M, MetaField, mapped_field
22+
from .document_base import E, InnerDoc, M, MetaField, mapped_field
2323
from .exceptions import (
2424
ElasticsearchDslException,
2525
IllegalOperation,
@@ -135,6 +135,7 @@
135135
"Double",
136136
"DoubleRange",
137137
"DslBase",
138+
"E",
138139
"ElasticsearchDslException",
139140
"EmptySearch",
140141
"Facet",

elasticsearch/dsl/document_base.py

Lines changed: 176 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import json
1819
from datetime import date, datetime
1920
from fnmatch import fnmatch
2021
from typing import (
@@ -56,7 +57,163 @@ def __init__(self, *args: Any, **kwargs: Any):
5657
self.args, self.kwargs = args, kwargs
5758

5859

59-
class InstrumentedField:
60+
class InstrumentedExpression:
61+
"""Proxy object for a ES|QL expression."""
62+
63+
def __init__(self, expr: str):
64+
self._expr = expr
65+
66+
def _render_value(self, value: Any) -> str:
67+
if isinstance(value, InstrumentedExpression):
68+
return str(value)
69+
return json.dumps(value)
70+
71+
def __str__(self) -> str:
72+
return self._expr
73+
74+
def __repr__(self) -> str:
75+
return f"InstrumentedExpression[{self._expr}]"
76+
77+
def __pos__(self) -> "InstrumentedExpression":
78+
return self
79+
80+
def __neg__(self) -> "InstrumentedExpression":
81+
return InstrumentedExpression(f"-({self._expr})")
82+
83+
def __eq__(self, value: Any) -> "InstrumentedExpression": # type: ignore[override]
84+
return InstrumentedExpression(f"{self._expr} == {self._render_value(value)}")
85+
86+
def __ne__(self, value: Any) -> "InstrumentedExpression": # type: ignore[override]
87+
return InstrumentedExpression(f"{self._expr} != {self._render_value(value)}")
88+
89+
def __lt__(self, value: Any) -> "InstrumentedExpression":
90+
return InstrumentedExpression(f"{self._expr} < {self._render_value(value)}")
91+
92+
def __gt__(self, value: Any) -> "InstrumentedExpression":
93+
return InstrumentedExpression(f"{self._expr} > {self._render_value(value)}")
94+
95+
def __le__(self, value: Any) -> "InstrumentedExpression":
96+
return InstrumentedExpression(f"{self._expr} <= {self._render_value(value)}")
97+
98+
def __ge__(self, value: Any) -> "InstrumentedExpression":
99+
return InstrumentedExpression(f"{self._expr} >= {self._render_value(value)}")
100+
101+
def __add__(self, value: Any) -> "InstrumentedExpression":
102+
return InstrumentedExpression(f"{self._expr} + {self._render_value(value)}")
103+
104+
def __radd__(self, value: Any) -> "InstrumentedExpression":
105+
return InstrumentedExpression(f"{self._render_value(value)} + {self._expr}")
106+
107+
def __sub__(self, value: Any) -> "InstrumentedExpression":
108+
return InstrumentedExpression(f"{self._expr} - {self._render_value(value)}")
109+
110+
def __rsub__(self, value: Any) -> "InstrumentedExpression":
111+
return InstrumentedExpression(f"{self._render_value(value)} - {self._expr}")
112+
113+
def __mul__(self, value: Any) -> "InstrumentedExpression":
114+
return InstrumentedExpression(f"{self._expr} * {self._render_value(value)}")
115+
116+
def __rmul__(self, value: Any) -> "InstrumentedExpression":
117+
return InstrumentedExpression(f"{self._render_value(value)} * {self._expr}")
118+
119+
def __truediv__(self, value: Any) -> "InstrumentedExpression":
120+
return InstrumentedExpression(f"{self._expr} / {self._render_value(value)}")
121+
122+
def __rtruediv__(self, value: Any) -> "InstrumentedExpression":
123+
return InstrumentedExpression(f"{self._render_value(value)} / {self._expr}")
124+
125+
def __mod__(self, value: Any) -> "InstrumentedExpression":
126+
return InstrumentedExpression(f"{self._expr} % {self._render_value(value)}")
127+
128+
def __rmod__(self, value: Any) -> "InstrumentedExpression":
129+
return InstrumentedExpression(f"{self._render_value(value)} % {self._expr}")
130+
131+
def is_null(self) -> "InstrumentedExpression":
132+
"""Compare the expression against NULL."""
133+
return InstrumentedExpression(f"{self._expr} IS NULL")
134+
135+
def is_not_null(self) -> "InstrumentedExpression":
136+
"""Compare the expression against NOT NULL."""
137+
return InstrumentedExpression(f"{self._expr} IS NOT NULL")
138+
139+
def in_(self, *values: Any) -> "InstrumentedExpression":
140+
"""Test if the expression equals one of the given values."""
141+
rendered_values = ", ".join([f"{value}" for value in values])
142+
return InstrumentedExpression(f"{self._expr} IN ({rendered_values})")
143+
144+
def like(self, *patterns: str) -> "InstrumentedExpression":
145+
"""Filter the expression using a string pattern."""
146+
if len(patterns) == 1:
147+
return InstrumentedExpression(
148+
f"{self._expr} LIKE {self._render_value(patterns[0])}"
149+
)
150+
else:
151+
return InstrumentedExpression(
152+
f'{self._expr} LIKE ({", ".join([self._render_value(p) for p in patterns])})'
153+
)
154+
155+
def rlike(self, *patterns: str) -> "InstrumentedExpression":
156+
"""Filter the expression using a regular expression."""
157+
if len(patterns) == 1:
158+
return InstrumentedExpression(
159+
f"{self._expr} RLIKE {self._render_value(patterns[0])}"
160+
)
161+
else:
162+
return InstrumentedExpression(
163+
f'{self._expr} RLIKE ({", ".join([self._render_value(p) for p in patterns])})'
164+
)
165+
166+
def match(self, query: str) -> "InstrumentedExpression":
167+
"""Perform a match query on the field."""
168+
return InstrumentedExpression(f"{self._expr}:{self._render_value(query)}")
169+
170+
def asc(self) -> "InstrumentedExpression":
171+
"""Return the field name representation for ascending sort order.
172+
173+
For use in ES|QL queries only.
174+
"""
175+
return InstrumentedExpression(f"{self._expr} ASC")
176+
177+
def desc(self) -> "InstrumentedExpression":
178+
"""Return the field name representation for descending sort order.
179+
180+
For use in ES|QL queries only.
181+
"""
182+
return InstrumentedExpression(f"{self._expr} DESC")
183+
184+
def nulls_first(self) -> "InstrumentedExpression":
185+
"""Return the field name representation for nulls first sort order.
186+
187+
For use in ES|QL queries only.
188+
"""
189+
return InstrumentedExpression(f"{self._expr} NULLS FIRST")
190+
191+
def nulls_last(self) -> "InstrumentedExpression":
192+
"""Return the field name representation for nulls last sort order.
193+
194+
For use in ES|QL queries only.
195+
"""
196+
return InstrumentedExpression(f"{self._expr} NULLS LAST")
197+
198+
def where(
199+
self, *expressions: Union[str, "InstrumentedExpression"]
200+
) -> "InstrumentedExpression":
201+
"""Add a condition to be met for the row to be included.
202+
203+
Use only in expressions given in the ``STATS`` command.
204+
"""
205+
if len(expressions) == 1:
206+
return InstrumentedExpression(f"{self._expr} WHERE {expressions[0]}")
207+
else:
208+
return InstrumentedExpression(
209+
f'{self._expr} WHERE {" AND ".join([f"({expr})" for expr in expressions])}'
210+
)
211+
212+
213+
E = InstrumentedExpression
214+
215+
216+
class InstrumentedField(InstrumentedExpression):
60217
"""Proxy object for a mapped document field.
61218
62219
An object of this instance is returned when a field is accessed as a class
@@ -71,8 +228,8 @@ class MyDocument(Document):
71228
s = s.sort(-MyDocument.name) # sort by name in descending order
72229
"""
73230

74-
def __init__(self, name: str, field: Field):
75-
self._name = name
231+
def __init__(self, name: str, field: Optional[Field]):
232+
super().__init__(name)
76233
self._field = field
77234

78235
# note that the return value type here assumes classes will only be used to
@@ -83,26 +240,29 @@ def __getattr__(self, attr: str) -> "InstrumentedField":
83240
# first let's see if this is an attribute of this object
84241
return super().__getattribute__(attr) # type: ignore[no-any-return]
85242
except AttributeError:
86-
try:
87-
# next we see if we have a sub-field with this name
88-
return InstrumentedField(f"{self._name}.{attr}", self._field[attr])
89-
except KeyError:
90-
# lastly we let the wrapped field resolve this attribute
91-
return getattr(self._field, attr) # type: ignore[no-any-return]
92-
93-
def __pos__(self) -> str:
243+
if self._field:
244+
try:
245+
# next we see if we have a sub-field with this name
246+
return InstrumentedField(f"{self._expr}.{attr}", self._field[attr])
247+
except KeyError:
248+
# lastly we let the wrapped field resolve this attribute
249+
return getattr(self._field, attr) # type: ignore[no-any-return]
250+
else:
251+
raise
252+
253+
def __pos__(self) -> str: # type: ignore[override]
94254
"""Return the field name representation for ascending sort order"""
95-
return f"{self._name}"
255+
return f"{self._expr}"
96256

97-
def __neg__(self) -> str:
257+
def __neg__(self) -> str: # type: ignore[override]
98258
"""Return the field name representation for descending sort order"""
99-
return f"-{self._name}"
259+
return f"-{self._expr}"
100260

101261
def __str__(self) -> str:
102-
return self._name
262+
return self._expr
103263

104264
def __repr__(self) -> str:
105-
return f"InstrumentedField[{self._name}]"
265+
return f"InstrumentedField[{self._expr}]"
106266

107267

108268
class DocumentMeta(type):

elasticsearch/dsl/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def __init__(self, _expand__to_dot: Optional[bool] = None, **params: Any) -> Non
333333
_expand__to_dot = EXPAND__TO_DOT
334334
self._params: Dict[str, Any] = {}
335335
for pname, pvalue in params.items():
336-
if pvalue == DEFAULT:
336+
if pvalue is DEFAULT:
337337
continue
338338
# expand "__" to dots
339339
if "__" in pname and _expand__to_dot:

elasticsearch/esql/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from .esql import ESQL, and_, not_, or_ # noqa: F401

0 commit comments

Comments
 (0)