KUDU-721: [Python] Add DECIMAL column type support This patch adds basic support to the Python client to create, read, and write tables with DECIMAL columns.
Change-Id: I8e0855100ab1ea891f990931ec94d0b98c0dece1 Reviewed-on: http://gerrit.cloudera.org:8080/9496 Tested-by: Kudu Jenkins Reviewed-by: David Ribeiro Alves <davidral...@gmail.com> Project: http://git-wip-us.apache.org/repos/asf/kudu/repo Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/e6aedc99 Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/e6aedc99 Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/e6aedc99 Branch: refs/heads/master Commit: e6aedc99d25ba8bc8f1fc37259404d55c802953f Parents: 2b68040 Author: Grant Henke <granthe...@gmail.com> Authored: Mon Mar 5 14:23:08 2018 -0600 Committer: David Ribeiro Alves <davidral...@gmail.com> Committed: Wed Mar 7 19:46:57 2018 +0000 ---------------------------------------------------------------------- python/kudu/__init__.py | 2 +- python/kudu/client.pyx | 20 +++++- python/kudu/libkudu_client.pxd | 36 ++++++++++- python/kudu/schema.pxd | 8 +++ python/kudu/schema.pyx | 106 +++++++++++++++++++++++++++++-- python/kudu/tests/test_scanner.py | 4 ++ python/kudu/tests/test_scantoken.py | 4 ++ python/kudu/tests/test_schema.py | 40 ++++++++++++ python/kudu/tests/test_util.py | 80 +++++++++++++++++++++++ python/kudu/tests/util.py | 14 +++- python/kudu/util.py | 65 +++++++++++++++++++ 11 files changed, 368 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/__init__.py ---------------------------------------------------------------------- diff --git a/python/kudu/__init__.py b/python/kudu/__init__.py index 8f0c8d9..75260d6 100644 --- a/python/kudu/__init__.py +++ b/python/kudu/__init__.py @@ -36,7 +36,7 @@ from kudu.errors import (KuduException, KuduBadStatus, KuduNotFound, # noqa from kudu.schema import (int8, int16, int32, int64, string_ as string, # noqa double_ as double, float_, float_ as float, binary, - unixtime_micros, bool_ as bool, + unixtime_micros, bool_ as bool, decimal, KuduType, SchemaBuilder, ColumnSpec, Schema, ColumnSchema, COMPRESSION_DEFAULT, http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/client.pyx ---------------------------------------------------------------------- diff --git a/python/kudu/client.pyx b/python/kudu/client.pyx index 0b28856..34cb53a 100644 --- a/python/kudu/client.pyx +++ b/python/kudu/client.pyx @@ -29,7 +29,8 @@ from libkudu_client cimport * from kudu.compat import tobytes, frombytes, dict_iter from kudu.schema cimport Schema, ColumnSchema, ColumnSpec, KuduValue, KuduType from kudu.errors cimport check_status -from kudu.util import to_unixtime_micros, from_unixtime_micros, from_hybridtime +from kudu.util import to_unixtime_micros, from_unixtime_micros, \ + from_hybridtime, to_unscaled_decimal, from_unscaled_decimal from errors import KuduException import six @@ -64,7 +65,8 @@ cdef dict _type_names = { KUDU_FLOAT : "KUDU_FLOAT", KUDU_DOUBLE : "KUDU_DOUBLE", KUDU_BINARY : "KUDU_BINARY", - KUDU_UNIXTIME_MICROS : "KUDU_UNIXTIME_MICROS" + KUDU_UNIXTIME_MICROS : "KUDU_UNIXTIME_MICROS", + KUDU_DECIMAL : "KUDU_DECIMAL" } # Range Partition Bound Type enums @@ -1314,6 +1316,15 @@ cdef class Row: check_status(self.row.GetUnixTimeMicros(i, &val)) return val + cdef inline __get_unscaled_decimal(self, int i): + cdef int128_t val + check_status(self.row.GetUnscaledDecimal(i, &val)) + return val + + cdef inline get_decimal(self, int i): + scale = self.parent.batch.projection_schema().Column(i).type_attributes().scale() + return from_unscaled_decimal(self.__get_unscaled_decimal(i), scale) + cdef inline get_slot(self, int i): cdef: Status s @@ -1339,6 +1350,8 @@ cdef class Row: return self.get_binary(i) elif t == KUDU_UNIXTIME_MICROS: return from_unixtime_micros(self.get_unixtime_micros(i)) + elif t == KUDU_DECIMAL: + return self.get_decimal(i) else: raise TypeError("Cannot get kudu type <{0}>" .format(_type_names[t])) @@ -2451,6 +2464,9 @@ cdef class PartialRow: elif t == KUDU_UNIXTIME_MICROS: check_status(self.row.SetUnixTimeMicros(i, <int64_t> to_unixtime_micros(value))) + elif t == KUDU_DECIMAL: + check_status(self.row.SetUnscaledDecimal(i, <int128_t> + to_unscaled_decimal(value))) else: raise TypeError("Cannot set kudu type <{0}>.".format(_type_names[t])) http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/libkudu_client.pxd ---------------------------------------------------------------------- diff --git a/python/kudu/libkudu_client.pxd b/python/kudu/libkudu_client.pxd index 9d1dfb7..b834bf0 100644 --- a/python/kudu/libkudu_client.pxd +++ b/python/kudu/libkudu_client.pxd @@ -62,6 +62,8 @@ cdef extern from "kudu/util/status.h" namespace "kudu" nogil: c_bool IsNotAuthorized() c_bool IsAborted() +cdef extern from "kudu/util/int128.h" namespace "kudu": + ctypedef int int128_t cdef extern from "kudu/util/monotime.h" namespace "kudu" nogil: @@ -119,6 +121,7 @@ cdef extern from "kudu/client/schema.h" namespace "kudu::client" nogil: KUDU_DOUBLE " kudu::client::KuduColumnSchema::DOUBLE" KUDU_BINARY " kudu::client::KuduColumnSchema::BINARY" KUDU_UNIXTIME_MICROS " kudu::client::KuduColumnSchema::UNIXTIME_MICROS" + KUDU_DECIMAL " kudu::client::KuduColumnSchema::DECIMAL" enum EncodingType" kudu::client::KuduColumnStorageAttributes::EncodingType": EncodingType_AUTO " kudu::client::KuduColumnStorageAttributes::AUTO_ENCODING" @@ -142,6 +145,17 @@ cdef extern from "kudu/client/schema.h" namespace "kudu::client" nogil: CompressionType compression string ToString() + cdef cppclass KuduColumnTypeAttributes: + KuduColumnTypeAttributes() + KuduColumnTypeAttributes(const KuduColumnTypeAttributes& other) + KuduColumnTypeAttributes(int8_t precision, int8_t scale) + + int8_t precision() + int8_t scale() + + c_bool Equals(KuduColumnTypeAttributes& other) + void CopyFrom(KuduColumnTypeAttributes& other) + cdef cppclass KuduColumnSchema: KuduColumnSchema(const KuduColumnSchema& other) KuduColumnSchema(const string& name, DataType type) @@ -152,6 +166,7 @@ cdef extern from "kudu/client/schema.h" namespace "kudu::client" nogil: string& name() c_bool is_nullable() DataType type() + KuduColumnTypeAttributes type_attributes() c_bool Equals(KuduColumnSchema& other) void CopyFrom(KuduColumnSchema& other) @@ -183,6 +198,9 @@ cdef extern from "kudu/client/schema.h" namespace "kudu::client" nogil: KuduColumnSpec* Nullable() KuduColumnSpec* Type(DataType type_) + KuduColumnSpec* Precision(int8_t precision); + KuduColumnSpec* Scale(int8_t scale); + KuduColumnSpec* RenameTo(const string& new_name) @@ -227,8 +245,6 @@ cdef extern from "kudu/client/scan_batch.h" namespace "kudu::client" nogil: Status GetUnixTimeMicros(int col_idx, int64_t* micros_since_utc_epoch) - Status GetString(Slice& col_name, Slice* val) - Status GetString(int col_idx, Slice* val) Status GetFloat(Slice& col_name, float* val) Status GetFloat(int col_idx, float* val) @@ -236,6 +252,12 @@ cdef extern from "kudu/client/scan_batch.h" namespace "kudu::client" nogil: Status GetDouble(Slice& col_name, double* val) Status GetDouble(int col_idx, double* val) + Status GetUnscaledDecimal(Slice& col_name, int128_t* val) + Status GetUnscaledDecimal(int col_idx, int128_t* val) + + Status GetString(Slice& col_name, Slice* val) + Status GetString(int col_idx, Slice* val) + Status GetBinary(const Slice& col_name, Slice* val) Status GetBinary(int col_idx, Slice* val) @@ -306,6 +328,8 @@ cdef extern from "kudu/common/partial_row.h" namespace "kudu" nogil: Status SetDouble(Slice& col_name, double val) Status SetFloat(Slice& col_name, float val) + Status SetUnscaledDecimal(const Slice& col_name, int128_t val) + # Integer setters Status SetBool(int col_idx, c_bool val) @@ -317,6 +341,8 @@ cdef extern from "kudu/common/partial_row.h" namespace "kudu" nogil: Status SetDouble(int col_idx, double val) Status SetFloat(int col_idx, float val) + Status SetUnscaledDecimal(int col_idx, int128_t val) + # Set, but does not copy string Status SetString(Slice& col_name, Slice& val) Status SetString(int col_idx, Slice& val) @@ -370,6 +396,9 @@ cdef extern from "kudu/common/partial_row.h" namespace "kudu" nogil: Status GetFloat(Slice& col_name, float* val) Status GetFloat(int col_idx, float* val) + Status GetUnscaledDecimal(Slice& col_name, int128_t* val) + Status GetUnscaledDecimal(int col_idx, int128_t* val) + # Gets the string but does not copy the value. Callers should # copy the resulting Slice if necessary. Status GetString(Slice& col_name, Slice* val) @@ -451,6 +480,9 @@ cdef extern from "kudu/client/value.h" namespace "kudu::client" nogil: C_KuduValue* FromBool(c_bool val); @staticmethod + C_KuduValue* FromDecimal(int128_t val, int8_t scale); + + @staticmethod C_KuduValue* CopyString(const Slice& s); http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/schema.pxd ---------------------------------------------------------------------- diff --git a/python/kudu/schema.pxd b/python/kudu/schema.pxd index c1cfc2e..56a4068 100644 --- a/python/kudu/schema.pxd +++ b/python/kudu/schema.pxd @@ -26,6 +26,14 @@ cdef class KuduType(object): DataType type +cdef class ColumnTypeAttributes: + """ + Wraps a Kudu client ColumnTypeAttributes object + """ + cdef: + KuduColumnTypeAttributes* type_attributes + + cdef class ColumnSchema: """ Wraps a Kudu client ColumnSchema object http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/schema.pyx ---------------------------------------------------------------------- diff --git a/python/kudu/schema.pyx b/python/kudu/schema.pyx index 558fcf3..ed17378 100644 --- a/python/kudu/schema.pyx +++ b/python/kudu/schema.pyx @@ -24,7 +24,7 @@ from kudu.compat import tobytes, frombytes from kudu.schema cimport * from kudu.errors cimport check_status from kudu.client cimport PartialRow -from kudu.util import to_unixtime_micros +from kudu.util import get_decimal_scale, to_unixtime_micros, to_unscaled_decimal import six @@ -44,6 +44,7 @@ DOUBLE = KUDU_DOUBLE UNIXTIME_MICROS = KUDU_UNIXTIME_MICROS BINARY = KUDU_BINARY +DECIMAL = KUDU_DECIMAL cdef dict _reverse_dict(d): return dict((v, k) for k, v in d.items()) @@ -118,6 +119,7 @@ float_ = KuduType(KUDU_FLOAT) double_ = KuduType(KUDU_DOUBLE) binary = KuduType(KUDU_BINARY) unixtime_micros = KuduType(KUDU_UNIXTIME_MICROS) +decimal = KuduType(KUDU_DECIMAL) cdef dict _type_names = { @@ -130,7 +132,8 @@ cdef dict _type_names = { FLOAT: 'float', DOUBLE: 'double', BINARY: 'binary', - UNIXTIME_MICROS: 'unixtime_micros' + UNIXTIME_MICROS: 'unixtime_micros', + DECIMAL: 'decimal' } @@ -146,7 +149,8 @@ cdef dict _type_to_obj = { FLOAT: float_, DOUBLE: double_, BINARY: binary, - UNIXTIME_MICROS: unixtime_micros + UNIXTIME_MICROS: unixtime_micros, + DECIMAL: decimal } @@ -160,6 +164,40 @@ cdef KuduType to_data_type(object obj): else: raise ValueError('Invalid type: {0}'.format(obj)) +cdef cppclass KuduColumnTypeAttributes: + KuduColumnTypeAttributes() + KuduColumnTypeAttributes(const KuduColumnTypeAttributes& other) + KuduColumnTypeAttributes(int8_t precision, int8_t scale) + + int8_t precision() + int8_t scale() + + c_bool Equals(KuduColumnTypeAttributes& other) + void CopyFrom(KuduColumnTypeAttributes& other) + +cdef class ColumnTypeAttributes: + """ + Wraps a Kudu client ColumnTypeAttributes object. + """ + def __cinit__(self): + self.type_attributes = NULL + + def __dealloc__(self): + if self.type_attributes is not NULL: + del self.type_attributes + + property precision: + def __get__(self): + return self.type_attributes.precision() + + property scale: + def __get__(self): + return self.type_attributes.scale() + + def __repr__(self): + return ('ColumnTypeAttributes(precision=%s, scale=%s)' + % (self.type_attributes.precision(), + self.type_attributes.scale())) cdef class ColumnSchema: """ @@ -189,6 +227,12 @@ cdef class ColumnSchema: def __get__(self): return self.schema.is_nullable() + property type_attributes: + def __get__(self): + cdef ColumnTypeAttributes result = ColumnTypeAttributes() + result.type_attributes = new KuduColumnTypeAttributes(self.schema.type_attributes()) + return result + def equals(self, other): if not isinstance(other, ColumnSchema): return False @@ -295,6 +339,46 @@ cdef class ColumnSpec: self.spec.Encoding(type) return self + def precision(self, precision): + """ + Set the precision for the column. + + Clients must specify a precision for decimal columns. Precision is the total + number of digits that can be represented by the column, regardless of the + location of the decimal point. For example, representing integer values up to 9999, + and fractional values up to 99.99, both require a precision of 4. You can also + represent corresponding negative values, without any change in the precision. + For example, the range -9999 to 9999 still only requires a precision of 4. + + The precision must be between 1 and 38. + + Returns + ------- + self + """ + self.spec.Precision(precision) + return self + + def scale(self, scale): + """ + Set the scale for the column. + + Clients can specify a scale for decimal columns. Scale represents the number + of fractional digits. This value must be less than or equal to precision. + A scale of 0 produces integral values, with no fractional part. If precision + and scale are equal, all the digits come after the decimal point, making all + the values between 0.9999 and -0.9999. + + The scale must be greater than 0 and less than the column's precision. + If no scale is provided a default scale of 0 is used. + + Returns + ------- + self + """ + self.spec.Scale(scale) + return self + def primary_key(self): """ Make this column a primary key. If you use this method, it will be the @@ -386,7 +470,7 @@ cdef class SchemaBuilder: def add_column(self, name, type_=None, nullable=None, compression=None, encoding=None, primary_key=False, block_size=None, - default= None): + default=None, precision=None, scale=None): """ Add a new column to the schema. Returns a ColumnSpec object for further configuration and use in a fluid programming style. @@ -411,6 +495,10 @@ cdef class SchemaBuilder: Block size (in bytes) to use for the target column. default : obj Use this to set the column default value + precision : int + Use this precision for the decimal column + scale : int + Use this scale for the decimal column Examples -------- @@ -440,6 +528,12 @@ cdef class SchemaBuilder: if encoding is not None: result.encoding(encoding) + if precision is not None: + result.precision(precision) + + if scale is not None: + result.scale(scale) + if primary_key: result.primary_key() @@ -667,6 +761,10 @@ cdef class KuduValue: elif (type_.name == 'unixtime_micros'): value = to_unixtime_micros(value) self._value = C_KuduValue.FromInt(value) + elif (type_.name == 'decimal'): + scale = get_decimal_scale(value) + value = to_unscaled_decimal(value) + self._value = C_KuduValue.FromDecimal(value, scale) else: raise TypeError("Cannot initialize KuduValue for kudu type <{0}>" .format(type_.name)) http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/tests/test_scanner.py ---------------------------------------------------------------------- diff --git a/python/kudu/tests/test_scanner.py b/python/kudu/tests/test_scanner.py index 2db0411..fa94ea3 100644 --- a/python/kudu/tests/test_scanner.py +++ b/python/kudu/tests/test_scanner.py @@ -260,6 +260,10 @@ class TestScanner(TestScanBase): # Does a row check count only self._test_float_pred() + def test_decimal_pred(self): + # Test a decimal predicate + self._test_decimal_pred() + def test_binary_pred(self): # Test a binary predicate self._test_binary_pred() http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/tests/test_scantoken.py ---------------------------------------------------------------------- diff --git a/python/kudu/tests/test_scantoken.py b/python/kudu/tests/test_scantoken.py index c675a92..c274668 100644 --- a/python/kudu/tests/test_scantoken.py +++ b/python/kudu/tests/test_scantoken.py @@ -242,6 +242,10 @@ class TestScanToken(TestScanBase): # Test unixtime_micros value predicate self._test_unixtime_micros_pred() + def test_decimal_pred(self): + # Test decimal value predicate + self._test_decimal_pred() + def test_bool_pred(self): # Test a boolean value predicate self._test_bool_pred() http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/tests/test_schema.py ---------------------------------------------------------------------- diff --git a/python/kudu/tests/test_schema.py b/python/kudu/tests/test_schema.py index ee89452..4870ab7 100644 --- a/python/kudu/tests/test_schema.py +++ b/python/kudu/tests/test_schema.py @@ -142,6 +142,46 @@ class TestSchema(unittest.TestCase): # TODO(wesm): The C++ client does not give us an API to see the storage # attributes of a column + def test_decimal(self): + builder = kudu.schema_builder() + (builder.add_column('key') + .type('decimal') + .primary_key() + .nullable(False) + .precision(9) + .scale(2)) + schema = builder.build() + + column = schema[0] + tp = column.type + assert tp.name == 'decimal' + assert tp.type == kudu.schema.DECIMAL + ta = column.type_attributes + assert ta.precision == 9 + assert ta.scale == 2 + + def test_decimal_without_precision(self): + builder = kudu.schema_builder() + (builder.add_column('key') + .type('decimal') + .primary_key() + .nullable(False)) + + with self.assertRaises(kudu.KuduInvalidArgument): + builder.build() + + def test_precision_on_non_decimal_column(self): + builder = kudu.schema_builder() + (builder.add_column('key') + .type('int32') + .primary_key() + .nullable(False) + .precision(9) + .scale(2)) + + with self.assertRaises(kudu.KuduInvalidArgument): + builder.build() + def test_unsupported_col_spec_methods_for_create_table(self): builder = kudu.schema_builder() builder.add_column('test', 'int64').rename('test') http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/tests/test_util.py ---------------------------------------------------------------------- diff --git a/python/kudu/tests/test_util.py b/python/kudu/tests/test_util.py new file mode 100644 index 0000000..0d649fc --- /dev/null +++ b/python/kudu/tests/test_util.py @@ -0,0 +1,80 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import division + +from kudu.compat import unittest +from kudu.util import * + + +class TestUtil(unittest.TestCase): + + def setUp(self): + context = getcontext() + # By default Decimal objects in Python use a precision of 28 + # Kudu can support up to 38. We support passing this context + # on a per call basis if the user wants to adjust this value. + context.prec = 38 + + def test_to_unscaled_decimal(self): + self.assertEqual(0, to_unscaled_decimal(Decimal('0'))) + self.assertEqual(12345, to_unscaled_decimal(Decimal('123.45'))) + self.assertEqual(-12345, to_unscaled_decimal(Decimal('-123.45'))) + self.assertEqual(12345, to_unscaled_decimal(Decimal('12345'))) + self.assertEqual(10000, to_unscaled_decimal(Decimal('10000'))) + self.assertEqual(10000, to_unscaled_decimal(Decimal('0.10000'))) + self.assertEqual(1, to_unscaled_decimal(Decimal('1'))) + self.assertEqual(1, to_unscaled_decimal(Decimal('.1'))) + self.assertEqual(1, to_unscaled_decimal(Decimal('0.1'))) + self.assertEqual(1, to_unscaled_decimal(Decimal('0.01'))) + self.assertEqual(999999999, to_unscaled_decimal(Decimal('0.999999999'))) + self.assertEqual(999999999999999999, to_unscaled_decimal(Decimal('0.999999999999999999'))) + self.assertEqual(99999999999999999999999999999999999999, + to_unscaled_decimal(Decimal('0.99999999999999999999999999999999999999'))) + + def test_from_unscaled_decimal(self): + self.assertEqual(0, from_unscaled_decimal(0, 0)) + self.assertEqual(Decimal('123.45'), from_unscaled_decimal(12345, 2)) + self.assertEqual(Decimal('-123.45'), from_unscaled_decimal(-12345, 2)) + self.assertEqual(Decimal('12345'), from_unscaled_decimal(12345, 0)) + self.assertEqual(Decimal('10000'), from_unscaled_decimal(10000, 0)) + self.assertEqual(Decimal('0.10000'), from_unscaled_decimal(10000, 5)) + self.assertEqual(Decimal('1'), from_unscaled_decimal(1, 0)) + self.assertEqual(Decimal('.1'), from_unscaled_decimal(1, 1)) + self.assertEqual(Decimal('0.1'), from_unscaled_decimal(1, 1)) + self.assertEqual(Decimal('0.01'), from_unscaled_decimal(1, 2)) + self.assertEqual(Decimal('0.999999999'), from_unscaled_decimal(999999999, 9)) + self.assertEqual(Decimal('0.999999999999999999'), + from_unscaled_decimal(999999999999999999, 18)) + self.assertEqual(Decimal('0.99999999999999999999999999999999999999'), + from_unscaled_decimal(99999999999999999999999999999999999999, 38)) + + def test_get_decimal_scale(self): + self.assertEqual(0, get_decimal_scale(Decimal('0'))) + self.assertEqual(2, get_decimal_scale(Decimal('123.45'))) + self.assertEqual(2, get_decimal_scale(Decimal('-123.45'))) + self.assertEqual(0, get_decimal_scale(Decimal('12345'))) + self.assertEqual(0, get_decimal_scale(Decimal('10000'))) + self.assertEqual(5, get_decimal_scale(Decimal('0.10000'))) + self.assertEqual(0, get_decimal_scale(Decimal('1'))) + self.assertEqual(1, get_decimal_scale(Decimal('.1'))) + self.assertEqual(1, get_decimal_scale(Decimal('0.1'))) + self.assertEqual(2, get_decimal_scale(Decimal('0.01'))) + self.assertEqual(9, get_decimal_scale(Decimal('0.999999999'))) + self.assertEqual(18, get_decimal_scale(Decimal('0.999999999999999999'))) + self.assertEqual(38, get_decimal_scale(Decimal('0.99999999999999999999999999999999999999'))) http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/tests/util.py ---------------------------------------------------------------------- diff --git a/python/kudu/tests/util.py b/python/kudu/tests/util.py index be7f6d8..e6d1334 100644 --- a/python/kudu/tests/util.py +++ b/python/kudu/tests/util.py @@ -17,6 +17,7 @@ # specific language governing permissions and limitations # under the License. +from decimal import Decimal from kudu.compat import unittest from kudu.client import Partitioning from kudu.tests.common import KuduTestBase @@ -71,6 +72,7 @@ class TestScanBase(KuduTestBase, unittest.TestCase): builder = kudu.schema_builder() builder.add_column('key').type(kudu.int64).nullable(False) builder.add_column('unixtime_micros_val', type_=kudu.unixtime_micros, nullable=False) + builder.add_column('decimal_val', type_=kudu.decimal, precision=5, scale=2) builder.add_column('string_val', type_=kudu.string, compression=kudu.COMPRESSION_LZ4, encoding='prefix') builder.add_column('bool_val', type_=kudu.bool) builder.add_column('double_val', type_=kudu.double) @@ -103,11 +105,11 @@ class TestScanBase(KuduTestBase, unittest.TestCase): # Insert new rows self.type_test_rows = [ - (1, datetime.datetime(2016, 1, 1).replace(tzinfo=pytz.utc), + (1, datetime.datetime(2016, 1, 1).replace(tzinfo=pytz.utc), Decimal('111.11'), "Test One", True, 1.7976931348623157 * (10^308), 127, b'\xce\x99\xce\xbf\xcf\x81\xce\xb4\xce\xb1\xce\xbd\xce\xaf\xce\xb1', 3.402823 * (10^38)), - (2, datetime.datetime.utcnow().replace(tzinfo=pytz.utc), + (2, datetime.datetime.utcnow().replace(tzinfo=pytz.utc), Decimal('0.99'), "æµè¯äº", False, 200.1, -1, b'\xd0\x98\xd0\xbe\xd1\x80\xd0\xb4\xd0\xb0\xd0\xbd\xd0\xb8\xd1\x8f', -150.2) @@ -231,6 +233,14 @@ class TestScanBase(KuduTestBase, unittest.TestCase): count_only=True ) + def _test_decimal_pred(self): + self.verify_pred_type_scans( + preds=[ + self.type_table['decimal_val'] == Decimal('111.11') + ], + row_indexes=slice(0, 1), + ) + def _test_binary_pred(self): self.verify_pred_type_scans( preds=[ http://git-wip-us.apache.org/repos/asf/kudu/blob/e6aedc99/python/kudu/util.py ---------------------------------------------------------------------- diff --git a/python/kudu/util.py b/python/kudu/util.py index 350a011..61062fb 100644 --- a/python/kudu/util.py +++ b/python/kudu/util.py @@ -17,6 +17,7 @@ import datetime import six +from decimal import Decimal, getcontext from pytz import utc @@ -82,6 +83,7 @@ def to_unixtime_micros(timestamp, format = "%Y-%m-%dT%H:%M:%S.%f"): td_micros = td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6 return int(td_micros) + def from_unixtime_micros(unixtime_micros): """ Convert the input unixtime_micros value to a datetime in UTC. @@ -101,6 +103,7 @@ def from_unixtime_micros(unixtime_micros): raise ValueError("Invalid unixtime_micros value." + "You must provide an integer value.") + def from_hybridtime(hybridtime): """ Convert a raw HybridTime value to a datetime in UTC. @@ -115,3 +118,65 @@ def from_hybridtime(hybridtime): """ # Add 1 so the value is usable for snapshot scans return from_unixtime_micros(int(hybridtime >> 12) + 1) + + +def to_unscaled_decimal(decimal, context=None): + """ + Convert incoming decimal value to a int representing + the unscaled decimal value. + + Parameters + --------- + decimal : Decimal + The decimal value to convert to an unscaled int + context : Context + The optional context to use + + Returns + ------- + int : The unscaled decimal int + """ + if context is None: + context = getcontext() + + scale = get_decimal_scale(decimal) + return decimal.scaleb(scale, context).to_integral_exact(None, context) + + +def from_unscaled_decimal(unscaled_decimal, scale, context=None): + """ + Convert the input unscaled_decimal value to a Decimal instance. + + Parameters + ---------- + unscaled_decimal : int + The unscaled int value of a decimal + scale : int + The scale that should be used when converting + context : Context + The optional context to use + + Returns + ------- + decimal : The scaled Decimal + """ + if context is None: + context = getcontext() + + return Decimal(unscaled_decimal, context).scaleb(-scale, context) + + +def get_decimal_scale(decimal): + """ + Get the scale of the decimal. + + Parameters + --------- + decimal : Decimal + The decimal value + + Returns + ------- + int : The calculated scale + """ + return max(0, -decimal.as_tuple().exponent)