diff --git a/src/crate/client/cursor.py b/src/crate/client/cursor.py index 59e936d7..10f732c2 100644 --- a/src/crate/client/cursor.py +++ b/src/crate/client/cursor.py @@ -21,6 +21,7 @@ from .exceptions import ProgrammingError import warnings +from datetime import datetime class Cursor(object): @@ -49,9 +50,47 @@ def execute(self, sql, parameters=None, bulk_parameters=None): self._result = self.connection.client.sql(sql, parameters, bulk_parameters) - if "rows" in self._result: + + if "rows" not in self._result: + return + + if "col_types" in self._result: + self.rows = iter(self._transform_result_types()) + + else: self.rows = iter(self._result["rows"]) + def _transform_result_types(self): + """ + Generate row items with column values converted to their corresponding + native Python types, based on information from `col_types`. + + Currently, only converting to native `datetime` objects is implemented. + """ + datetime_column_types = [11, 15] + datetime_columns_mask = [ + True if col_type in datetime_column_types else False + for col_type in self._result["col_types"] + ] + for row in self._result["rows"]: + yield list(self._transform_datetime_columns(row, iter(datetime_columns_mask))) + + @staticmethod + def _transform_datetime_columns(row, column_flags): + """ + Convert all designated columns to native Python `datetime` objects. + """ + for value in row: + try: + flag = next(column_flags) + except StopIteration: + break + + if flag and value is not None: + value = datetime.utcfromtimestamp(value / 1e3) + + yield value + def executemany(self, sql, seq_of_parameters): """ Prepare a database operation (query or command) and then execute it diff --git a/src/crate/client/doctests/client.txt b/src/crate/client/doctests/client.txt index 109c7401..dd78c1b0 100644 --- a/src/crate/client/doctests/client.txt +++ b/src/crate/client/doctests/client.txt @@ -212,9 +212,9 @@ supported, all other fields are 'None':: >>> result = cursor.fetchone() >>> pprint(result) ['Aldebaran', - 1658167836758, - 1658167836758, - 1658167836758, + datetime.datetime(2022, 7, 18, 18, 10, 36, 758000), + datetime.datetime(2022, 7, 18, 18, 10, 36, 758000), + datetime.datetime(2022, 7, 18, 18, 10, 36, 758000), None, None, 'Star System', @@ -239,6 +239,7 @@ supported, all other fields are 'None':: ('description', None, None, None, None, None, None), ('details', None, None, None, None, None, None)) + Closing the Cursor ================== diff --git a/src/crate/client/doctests/http.txt b/src/crate/client/doctests/http.txt index fa9407c3..0c411f55 100644 --- a/src/crate/client/doctests/http.txt +++ b/src/crate/client/doctests/http.txt @@ -69,7 +69,8 @@ Issue a select statement against our with test data pre-filled crate instance:: >>> http_client = HttpClient(crate_host) >>> result = http_client.sql('select name from locations order by name') >>> pprint(result) - {'cols': ['name'], + {'col_types': [4], + 'cols': ['name'], 'duration': ..., 'rowcount': 13, 'rows': [['Aldebaran'], diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 44643a36..e932f732 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -315,7 +315,7 @@ class Client(object): Crate connection client using CrateDB's HTTP API. """ - SQL_PATH = '/_sql' + SQL_PATH = '/_sql?types=true' """Crate URI path for issuing SQL statements.""" retry_interval = 30 @@ -385,7 +385,7 @@ def __init__(self, self.path = self.SQL_PATH if error_trace: - self.path += '?error_trace=true' + self.path += '&error_trace=true' def close(self): for server in self.server_pool.values(): diff --git a/src/crate/client/sqlalchemy/dialect.py b/src/crate/client/sqlalchemy/dialect.py index 637a8f92..45a9e803 100644 --- a/src/crate/client/sqlalchemy/dialect.py +++ b/src/crate/client/sqlalchemy/dialect.py @@ -89,6 +89,8 @@ def result_processor(self, dialect, coltype): def process(value): if not value: return + if isinstance(value, datetime): + return value.date() try: return datetime.utcfromtimestamp(value / 1e3).date() except TypeError: @@ -128,6 +130,8 @@ def result_processor(self, dialect, coltype): def process(value): if not value: return + if isinstance(value, datetime): + return value try: return datetime.utcfromtimestamp(value / 1e3) except TypeError: diff --git a/src/crate/client/sqlalchemy/doctests/itests.txt b/src/crate/client/sqlalchemy/doctests/itests.txt index f9e2d09e..9073f5da 100644 --- a/src/crate/client/sqlalchemy/doctests/itests.txt +++ b/src/crate/client/sqlalchemy/doctests/itests.txt @@ -80,9 +80,6 @@ Date should have been set at the insert due to default value via python method:: >>> dt.day == now.day True - >>> (now - location.datetime_tz).seconds < 4 - True - Verify the return type of date and datetime:: >>> type(location.date) diff --git a/src/crate/client/test_http.py b/src/crate/client/test_http.py index 4a073099..5c22c0b6 100644 --- a/src/crate/client/test_http.py +++ b/src/crate/client/test_http.py @@ -431,12 +431,13 @@ def test_params(self): client = Client(['127.0.0.1:4200'], error_trace=True) parsed = urlparse(client.path) params = parse_qs(parsed.query) + self.assertEqual(params["types"], ["true"]) self.assertEqual(params["error_trace"], ["true"]) client.close() def test_no_params(self): client = Client() - self.assertEqual(client.path, "/_sql") + self.assertEqual(client.path, "/_sql?types=true") client.close() diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index e0abafd2..d7d80e34 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -149,6 +149,7 @@ def refresh(table): def setUpWithCrateLayer(test): + test.globs['os'] = os test.globs['HttpClient'] = http.Client test.globs['crate_host'] = crate_host test.globs['pprint'] = pprint