Skip to content

Commit 02959e9

Browse files
authored
feat(query): add time_zone param (#69)
* feat(query): add time_zone param * docs(README): update README * fix(README): fix code
1 parent ea7a093 commit 02959e9

File tree

6 files changed

+117
-31
lines changed

6 files changed

+117
-31
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
run: |
8484
export ES_URI="http://localhost:9200"
8585
export ES_PORT=9200
86+
export ES_SUPPORT_DATETIME_PARSE=False
8687
nosetests -v --with-coverage --cover-package=es es.tests
8788
- name: Run tests on Elasticsearch 7.10.X
8889
run: |
@@ -97,6 +98,7 @@ jobs:
9798
export ES_PORT=19200
9899
export ES_SCHEME=https
99100
export ES_USER=admin
101+
export ES_SUPPORT_DATETIME_PARSE=False
100102
nosetests -v --with-coverage --cover-package=es es.tests
101103
- name: Run tests on Opendistro 13
102104
run: |
@@ -107,6 +109,7 @@ jobs:
107109
export ES_SCHEME=https
108110
export ES_USER=admin
109111
export ES_V2=True
112+
export ES_SUPPORT_DATETIME_PARSE=False
110113
nosetests -v --with-coverage --cover-package=es es.tests
111114
- name: Upload code coverage
112115
run: |

README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,28 @@
66
[![Coverage Status](https://codecov.io/github/preset-io/elasticsearch-dbapi/coverage.svg?branch=master)](https://codecov.io/github/preset-io/elasticsearch-dbapi)
77

88

9-
`elasticsearch-dbapi` Implements a DBAPI (PEP-249) and SQLAlchemy dialect,
10-
that enables SQL access on elasticsearch clusters for query only access.
9+
`elasticsearch-dbapi` Implements a DBAPI (PEP-249) and SQLAlchemy dialect,
10+
that enables SQL access on elasticsearch clusters for query only access.
1111

1212
On Elastic Elasticsearch:
1313
Uses Elastic X-Pack [SQL API](https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-sql.html)
1414

1515
On AWS ES, opendistro Elasticsearch:
16-
[Open Distro SQL](https://opendistro.github.io/for-elasticsearch-docs/docs/sql/)
16+
[Open Distro SQL](https://opendistro.github.io/for-elasticsearch-docs/docs/sql/)
1717

1818
This library supports Elasticsearch 7.X versions.
1919

2020
### Installation
2121

2222
```bash
2323
$ pip install elasticsearch-dbapi
24-
```
24+
```
2525

2626
To install support for AWS Elasticsearch Service / [Open Distro](https://opendistro.github.io/for-elasticsearch/features/SQL%20Support.html):
2727

2828
```bash
2929
$ pip install elasticsearch-dbapi[opendistro]
30-
```
30+
```
3131

3232
### Usage:
3333

@@ -92,7 +92,7 @@ print(logs.columns)
9292
[elasticsearch-py](https://elasticsearch-py.readthedocs.io/en/master/index.html)
9393
is used to establish connections and transport, this is the official
9494
elastic python library. `Elasticsearch` constructor accepts multiple optional parameters
95-
that can be used to properly configure your connection on aspects like security, performance
95+
that can be used to properly configure your connection on aspects like security, performance
9696
and high availability. These optional parameters can be set at the connection string, for
9797
example:
9898

@@ -112,16 +112,30 @@ The connection string follows RFC-1738, to support multiple nodes you should use
112112
By default the maximum number of rows which get fetched by a single query
113113
is limited to 10000. This can be adapted through the `fetch_size`
114114
parameter:
115+
115116
```python
116117
from es.elastic.api import connect
117118

118-
conn = connect(host='localhost')
119-
curs = conn.cursor(fetch_size=1000)
119+
conn = connect(host="localhost", fetch_size=1000)
120+
curs = conn.cursor()
120121
```
122+
121123
If more than 10000 rows should get fetched then
122124
[max_result_window](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#dynamic-index-settings)
123125
has to be adapted as well.
124126

127+
#### Time zone
128+
129+
By default, elasticsearch query time zone defaults to `Z` (UTC). This can be adapted through the `time_zone`
130+
parameter:
131+
132+
```python
133+
from es.elastic.api import connect
134+
135+
conn = connect(host="localhost", time_zone="Asia/Shanghai")
136+
curs = conn.cursor()
137+
```
138+
125139
### Tests
126140

127141
To run unittest launch elasticsearch and kibana (kibana is really not required but is a nice to have)
@@ -133,7 +147,7 @@ $ nosetests -v
133147

134148
### Special case for sql opendistro endpoint (AWS ES)
135149

136-
AWS ES exposes the opendistro SQL plugin, and it follows a different SQL dialect.
150+
AWS ES exposes the opendistro SQL plugin, and it follows a different SQL dialect.
137151
Using the `odelasticsearch` driver:
138152

139153
```python
@@ -203,7 +217,7 @@ Using the new SQL engine:
203217
Opendistro 1.13.0 brings (enabled by default) a new SQL engine, with lots of improvements and fixes.
204218
Take a look at the [release notes](https://github.com/opendistro-for-elasticsearch/sql/blob/develop/docs/dev/NewSQLEngine.md)
205219

206-
This DBAPI has to behave slightly different for SQL v1 and SQL v2, by default we comply with v1,
220+
This DBAPI has to behave slightly different for SQL v1 and SQL v2, by default we comply with v1,
207221
to enable v2 support, pass `v2=true` has a query parameter.
208222

209223
```
@@ -217,14 +231,14 @@ To connect to the provided Opendistro ES on `docker-compose` use the following U
217231

218232
This library does not yet support the following features:
219233

220-
- Array type columns are not supported. Elaticsearch SQL does not support them either.
234+
- Array type columns are not supported. Elaticsearch SQL does not support them either.
221235
SQLAlchemy `get_columns` will exclude them.
222236
- `object` and `nested` column types are not well supported and are converted to strings
223237
- Indexes that whose name start with `.`
224238
- GEO points are not currently well-supported and are converted to strings
225239

226240
- AWS ES (opendistro elascticsearch) is supported (still beta), known limitations are:
227-
* You are only able to `GROUP BY` keyword fields (new [experimental](https://github.com/opendistro-for-elasticsearch/sql#experimental)
241+
* You are only able to `GROUP BY` keyword fields (new [experimental](https://github.com/opendistro-for-elasticsearch/sql#experimental)
228242
opendistro SQL already supports it)
229-
* Indices with dots are not supported (indices like 'audit_log.2021.01.20'),
243+
* Indices with dots are not supported (indices like 'audit_log.2021.01.20'),
230244
on these cases we recommend the use of aliases

es/baseapi.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def get_description_from_columns(
109109

110110
class BaseConnection(object):
111111

112-
"""Connection to an ES Cluster """
112+
"""Connection to an ES Cluster"""
113113

114114
def __init__(
115115
self,
@@ -192,6 +192,7 @@ def __init__(self, url: str, es: Elasticsearch, **kwargs):
192192
self.es = es
193193
self.sql_path = kwargs.get("sql_path", DEFAULT_SQL_PATH)
194194
self.fetch_size = kwargs.get("fetch_size", DEFAULT_FETCH_SIZE)
195+
self.time_zone: Optional[str] = kwargs.get("time_zone")
195196
# This read/write attribute specifies the number of rows to fetch at a
196197
# time with .fetchmany(). It defaults to 1 meaning to fetch a single
197198
# row at a time.
@@ -218,7 +219,7 @@ def custom_sql_to_method_dispatcher(self, command: str) -> Optional["BaseCursor"
218219
@check_result
219220
@check_closed
220221
def rowcount(self) -> int:
221-
""" Counts the number of rows on a result """
222+
"""Counts the number of rows on a result"""
222223
if self._results:
223224
return len(self._results)
224225
return 0
@@ -230,7 +231,7 @@ def close(self) -> None:
230231

231232
@check_closed
232233
def execute(self, operation, parameters=None) -> "BaseCursor":
233-
""" Children must implement their own custom execute """
234+
"""Children must implement their own custom execute"""
234235
raise NotImplementedError # pragma: no cover
235236

236237
@check_closed
@@ -311,11 +312,13 @@ def elastic_query(self, query: str) -> Dict[str, Any]:
311312
payload = {"query": query}
312313
if self.fetch_size is not None:
313314
payload["fetch_size"] = self.fetch_size
315+
if self.time_zone is not None:
316+
payload["time_zone"] = self.time_zone
314317
path = f"/{self.sql_path}/"
315318
try:
316319
response = self.es.transport.perform_request("POST", path, body=payload)
317320
except es_exceptions.ConnectionError:
318-
raise exceptions.OperationalError(f"Error connecting to Elasticsearch")
321+
raise exceptions.OperationalError("Error connecting to Elasticsearch")
319322
except es_exceptions.RequestError as ex:
320323
raise exceptions.ProgrammingError(
321324
f"Error ({ex.error}): {ex.info['error']['reason']}"

es/elastic/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def connect(
3838

3939
class Connection(BaseConnection):
4040

41-
"""Connection to an ES Cluster """
41+
"""Connection to an ES Cluster"""
4242

4343
def __init__(
4444
self,

es/opendistro/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def connect(
4242

4343
class Connection(BaseConnection):
4444

45-
"""Connection to an ES Cluster """
45+
"""Connection to an ES Cluster"""
4646

4747
def __init__(
4848
self,

es/tests/test_dbapi.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,35 @@
77
from es.opendistro.api import connect as open_connect
88

99

10+
def convert_bool(value: str) -> bool:
11+
return True if value == "True" else False
12+
13+
1014
class TestDBAPI(unittest.TestCase):
1115
def setUp(self):
1216
self.driver_name = os.environ.get("ES_DRIVER", "elasticsearch")
13-
host = os.environ.get("ES_HOST", "localhost")
14-
port = int(os.environ.get("ES_PORT", 9200))
15-
scheme = os.environ.get("ES_SCHEME", "http")
16-
verify_certs = os.environ.get("ES_VERIFY_CERTS", False)
17-
user = os.environ.get("ES_USER", None)
18-
password = os.environ.get("ES_PASSWORD", None)
17+
self.host = os.environ.get("ES_HOST", "localhost")
18+
self.port = int(os.environ.get("ES_PORT", 9200))
19+
self.scheme = os.environ.get("ES_SCHEME", "http")
20+
self.verify_certs = os.environ.get("ES_VERIFY_CERTS", False)
21+
self.user = os.environ.get("ES_USER", None)
22+
self.password = os.environ.get("ES_PASSWORD", None)
1923
self.v2 = bool(os.environ.get("ES_V2", False))
24+
self.support_datetime_parse = convert_bool(
25+
os.environ.get("ES_SUPPORT_DATETIME_PARSE", "True")
26+
)
2027

2128
if self.driver_name == "elasticsearch":
2229
self.connect_func = elastic_connect
2330
else:
2431
self.connect_func = open_connect
2532
self.conn = self.connect_func(
26-
host=host,
27-
port=port,
28-
scheme=scheme,
29-
verify_certs=verify_certs,
30-
user=user,
31-
password=password,
33+
host=self.host,
34+
port=self.port,
35+
scheme=self.scheme,
36+
verify_certs=self.verify_certs,
37+
user=self.user,
38+
password=self.password,
3239
v2=self.v2,
3340
)
3441
self.cursor = self.conn.cursor()
@@ -213,3 +220,62 @@ def test_https(self, mock_elasticsearch):
213220
mock_elasticsearch.assert_called_once_with(
214221
"https://localhost:9200/", http_auth=("user", "password")
215222
)
223+
224+
def test_simple_search_with_time_zone(self):
225+
"""
226+
DBAPI: Test simple search with time zone
227+
UTC -> CST
228+
2019-10-13T00:00:00.000Z => 2019-10-13T08:00:00.000+08:00
229+
2019-10-13T00:00:01.000Z => 2019-10-13T08:01:00.000+08:00
230+
2019-10-13T00:00:02.000Z => 2019-10-13T08:02:00.000+08:00
231+
"""
232+
233+
if not self.support_datetime_parse:
234+
return
235+
236+
conn = self.connect_func(
237+
host=self.host,
238+
port=self.port,
239+
scheme=self.scheme,
240+
verify_certs=self.verify_certs,
241+
user=self.user,
242+
password=self.password,
243+
v2=self.v2,
244+
time_zone="Asia/Shanghai",
245+
)
246+
cursor = conn.cursor()
247+
pattern = "yyyy-MM-dd HH:mm:ss"
248+
sql = f"""
249+
SELECT timestamp FROM data1
250+
WHERE timestamp >= DATETIME_PARSE('2019-10-13 00:08:00', '{pattern}')
251+
"""
252+
253+
rows = cursor.execute(sql).fetchall()
254+
self.assertEqual(len(rows), 3)
255+
256+
def test_simple_search_without_time_zone(self):
257+
"""
258+
DBAPI: Test simple search without time zone
259+
"""
260+
261+
if not self.support_datetime_parse:
262+
return
263+
264+
conn = self.connect_func(
265+
host=self.host,
266+
port=self.port,
267+
scheme=self.scheme,
268+
verify_certs=self.verify_certs,
269+
user=self.user,
270+
password=self.password,
271+
v2=self.v2,
272+
)
273+
cursor = conn.cursor()
274+
pattern = "yyyy-MM-dd HH:mm:ss"
275+
sql = f"""
276+
SELECT * FROM data1
277+
WHERE timestamp >= DATETIME_PARSE('2019-10-13 08:00:00', '{pattern}')
278+
"""
279+
280+
rows = cursor.execute(sql).fetchall()
281+
self.assertEqual(len(rows), 0)

0 commit comments

Comments
 (0)