diff --git a/ibis/expr/rules.py b/ibis/expr/rules.py index 9d811df664b4..22ade379e070 100644 --- a/ibis/expr/rules.py +++ b/ibis/expr/rules.py @@ -43,15 +43,15 @@ def get_result(self): return shape_like_args(self.args, promoted_type) def _get_type(self): - if util.any_of(self.args, ir.FloatingValue): + if util.any_of(self.args, ir.DecimalValue): + return _decimal_promoted_type(self.args) + elif util.any_of(self.args, ir.FloatingValue): if util.any_of(self.args, ir.DoubleValue): return 'double' else: return 'float' elif util.all_of(self.args, ir.IntegerValue): return self._get_int_type() - elif util.any_of(self.args, ir.DecimalValue): - return _decimal_promoted_type(self.args) else: raise NotImplementedError diff --git a/ibis/expr/tests/test_value_exprs.py b/ibis/expr/tests/test_value_exprs.py index 92ff6ffba8c9..149ebdb8b26c 100644 --- a/ibis/expr/tests/test_value_exprs.py +++ b/ibis/expr/tests/test_value_exprs.py @@ -890,3 +890,18 @@ def test_fillna_null(value, expected): def test_string_temporal_compare(op, left, right): result = op(left, right) assert result.type().equals(dt.boolean) + + +@pytest.mark.parametrize( + ('value', 'type', 'expected_type_class'), + [ + (2.21, 'decimal', dt.Decimal), + (3.14, 'double', dt.Double), + (4.2, 'int64', dt.Double), + (4, 'int64', dt.Int64), + ] +) +def test_decimal_modulo_output_type(value, type, expected_type_class): + t = ibis.table([('a', type)]) + expr = t.a % value + assert isinstance(expr.type(), expected_type_class) diff --git a/ibis/pandas/api.py b/ibis/pandas/api.py index 72fbc5dd89d3..7041acfbcfbe 100644 --- a/ibis/pandas/api.py +++ b/ibis/pandas/api.py @@ -1,4 +1,7 @@ +from __future__ import absolute_import + from ibis.pandas.client import PandasClient +from ibis.pandas.decimal import execute_node # noqa: F401 from ibis.pandas.execution import execute # noqa: F401 diff --git a/ibis/pandas/client.py b/ibis/pandas/client.py index 2a468ca36e02..c85a378ab4ab 100644 --- a/ibis/pandas/client.py +++ b/ibis/pandas/client.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import six import numpy as np @@ -30,7 +32,7 @@ } -def pandas_dtypes_to_ibis_schema(df): +def pandas_dtypes_to_ibis_schema(df, schema): dtypes = df.dtypes pairs = [] @@ -41,10 +43,20 @@ def pandas_dtypes_to_ibis_schema(df): 'Column names must be strings to use the pandas backend' ) - if dtype == np.object_: - ibis_type = _INFERRED_DTYPE_TO_IBIS_TYPE[ - infer_dtype(df[column_name].dropna()) - ] + if column_name in schema: + ibis_type = dt.validate_type(schema[column_name]) + elif dtype == np.object_: + inferred_dtype = infer_dtype(df[column_name].dropna()) + + if inferred_dtype == 'mixed': + raise TypeError( + 'Unable to infer type of column {0!r}. Try instantiating ' + 'your table from the client with client.table(' + "'my_table', schema={{{0!r}: }})".format( + column_name + ) + ) + ibis_type = _INFERRED_DTYPE_TO_IBIS_TYPE[inferred_dtype] elif hasattr(dtype, 'tz'): ibis_type = dt.Timestamp(str(dtype.tz)) else: @@ -60,9 +72,11 @@ class PandasClient(client.Client): def __init__(self, dictionary): self.dictionary = dictionary - def table(self, name): + def table(self, name, schema=None): df = self.dictionary[name] - schema = pandas_dtypes_to_ibis_schema(df) + schema = pandas_dtypes_to_ibis_schema( + df, schema if schema is not None else {} + ) return ops.DatabaseTable(name, schema, self).to_expr() def execute(self, query, *args, **kwargs): diff --git a/ibis/pandas/core.py b/ibis/pandas/core.py index ab17801e693c..5193cb491542 100644 --- a/ibis/pandas/core.py +++ b/ibis/pandas/core.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import collections import numbers import datetime diff --git a/ibis/pandas/decimal.py b/ibis/pandas/decimal.py new file mode 100644 index 000000000000..849ebc5402fe --- /dev/null +++ b/ibis/pandas/decimal.py @@ -0,0 +1,121 @@ +from __future__ import absolute_import + +import decimal +import math +import numbers + +import numpy as np +import pandas as pd +import six + +import ibis.expr.datatypes as dt +import ibis.expr.operations as ops +from ibis.pandas.dispatch import execute_node + + +@execute_node.register(ops.Ln, decimal.Decimal) +def execute_decimal_natural_log(op, data, scope=None): + try: + return data.ln() + except decimal.InvalidOperation: + return decimal.Decimal('NaN') + + +@execute_node.register(ops.Log, decimal.Decimal, decimal.Decimal) +def execute_decimal_log_with_decimal_base(op, data, base, scope=None): + try: + return data.ln() / base.ln() + except decimal.InvalidOperation: + return decimal.Decimal('NaN') + + +@execute_node.register(ops.Log, decimal.Decimal, type(None)) +def execute_decimal_log_with_no_base(op, data, _, scope=None): + return execute_decimal_natural_log(op, data, scope=scope) + + +@execute_node.register(ops.Log, decimal.Decimal, numbers.Real) +def execute_decimal_log_with_real_base(op, data, base, scope=None): + return execute_node(op, data, decimal.Decimal(base), scope=scope) + + +@execute_node.register(ops.Log, decimal.Decimal, np.integer) +def execute_decimal_log_with_np_integer_base(op, data, base, scope=None): + return execute_node(op, data, int(base), scope=scope) + + +@execute_node.register(ops.Log2, decimal.Decimal) +def execute_decimal_log2(op, data, scope=None): + try: + return data.ln() / decimal.Decimal(2).ln() + except decimal.InvalidOperation: + return decimal.Decimal('NaN') + + +@execute_node.register(ops.UnaryOp, decimal.Decimal) +def execute_decimal_unary(op, data, scope=None): + operation_name = type(op).__name__.lower() + math_function = getattr(math, operation_name, None) + function = getattr( + decimal.Decimal, + operation_name, + lambda x: decimal.Decimal(math_function(x)) + ) + try: + return function(data) + except decimal.InvalidOperation: + return decimal.Decimal('NaN') + + +@execute_node.register(ops.Sign, decimal.Decimal) +def execute_decimal_sign(op, data, scope=None): + return data if not data else decimal.Decimal(1).copy_sign(data) + + +@execute_node.register(ops.Abs, decimal.Decimal) +def execute_decimal_abs(op, data, scope=None): + return abs(data) + + +@execute_node.register( + ops.Round, decimal.Decimal, (np.integer,) + six.integer_types +) +def execute_round_decimal(op, data, places, scope=None): + # If we only allowed Python 3, we wouldn't have to implement any of this; + # we could just call round(data, places) :( + tuple_value = data.as_tuple() + precision = len(tuple_value.digits) + integer_part_length = precision + min(tuple_value.exponent, 0) + + if places < 0: + decimal_format_string = '0.{}E+{:d}'.format( + '0' * (integer_part_length - 1 + places), + max(integer_part_length + places, abs(places)) + ) + else: + decimal_format_string = '{}.{}'.format( + '0' * integer_part_length, '0' * places + ) + + places = decimal.Decimal(decimal_format_string) + return data.quantize(places) + + +@execute_node.register(ops.Round, decimal.Decimal, type(None)) +def execute_round_decimal_no_places(op, data, _, scope=None): + return np.int64(round(data)) + + +@execute_node.register(ops.Cast, pd.Series, dt.Decimal) +def execute_cast_series_to_decimal(op, data, type, scope=None): + precision = type.precision + scale = type.scale + context = decimal.Context(prec=precision) + places = context.create_decimal( + '{}.{}'.format('0' * (precision - scale), '0' * scale), + ) + return data.apply( + lambda x, context=context, places=places: ( # noqa: E501 + context.create_decimal(x).quantize(places) + ) + ) diff --git a/ibis/pandas/dispatch.py b/ibis/pandas/dispatch.py index 07100c4c4a66..556e9a601275 100644 --- a/ibis/pandas/dispatch.py +++ b/ibis/pandas/dispatch.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from multipledispatch import Dispatcher diff --git a/ibis/pandas/execution.py b/ibis/pandas/execution.py index 69e4e30f8863..d51452f16aa8 100644 --- a/ibis/pandas/execution.py +++ b/ibis/pandas/execution.py @@ -1,7 +1,10 @@ +from __future__ import absolute_import + import numbers import operator import datetime import functools +import decimal import six @@ -135,6 +138,38 @@ def execute_cast_series_date(op, data, type, scope=None): } +@execute_node.register(ops.UnaryOp, pd.Series) +def execute_series_unary_op(op, data, scope=None): + function = getattr(np, type(op).__name__.lower()) + if data.dtype == np.dtype(np.object_): + return data.apply(functools.partial(execute_node, op, scope=scope)) + return function(data) + + +def vectorize_object(op, arg, *args, **kwargs): + func = np.vectorize(functools.partial(execute_node, op, **kwargs)) + return pd.Series(func(arg, *args), index=arg.index, name=arg.name) + + +@execute_node.register( + ops.Log, pd.Series, (pd.Series, numbers.Real, decimal.Decimal, type(None)) +) +def execute_series_log_with_base(op, data, base, scope=None): + if data.dtype == np.dtype(np.object_): + return vectorize_object(op, data, base, scope=scope) + + if base is None: + return np.log(data) + return np.log(data) / np.log(base) + + +@execute_node.register(ops.Ln, pd.Series) +def execute_series_natural_log(op, data, scope=None): + if data.dtype == np.dtype(np.object_): + return data.apply(functools.partial(execute_node, op, scope=scope)) + return np.log(data) + + @execute_node.register(ops.Cast, datetime.datetime, dt.String) def execute_cast_datetime_or_timestamp_to_string(op, data, type, scope=None): """Cast timestamps to strings""" @@ -213,6 +248,17 @@ def execute_cast_string_literal(op, data, type, scope=None): return cast_function(data) +@execute_node.register( + ops.Round, + pd.Series, + (pd.Series, np.integer, type(None)) + six.integer_types +) +def execute_round_series(op, data, places, scope=None): + if data.dtype == np.dtype(np.object_): + return vectorize_object(op, data, places, scope=scope) + return data.round(places if places is not None else 0) + + @execute_node.register(ops.TableColumn, (pd.DataFrame, DataFrameGroupBy)) def execute_table_column_dataframe_or_dataframe_groupby(op, data, scope=None): return data[op.name] diff --git a/ibis/pandas/tests/test_operations.py b/ibis/pandas/tests/test_operations.py index 5c1e6096ac38..7f02d36bab4d 100644 --- a/ibis/pandas/tests/test_operations.py +++ b/ibis/pandas/tests/test_operations.py @@ -1,5 +1,10 @@ +import math import operator import datetime +import decimal +import functools + +from operator import methodcaller import pytest @@ -31,7 +36,7 @@ def df(tz): ).dt.tz_localize(tz), 'dup_strings': list('dad'), 'dup_ints': [1, 2, 1], - 'float64_as_strings': ['1.0', '2', '3.234'], + 'float64_as_strings': ['100.01', '234.23', '-999.34'], 'int64_as_strings': list(map(str, range(1, 4))), 'strings_with_space': [' ', 'abab', 'ddeeffgg'], 'int64_with_zeros': [0, 1, 0], @@ -40,6 +45,7 @@ def df(tz): 'datetime_strings': pd.Series( pd.date_range(start='2017-01-02 01:02:03.234', periods=3).values, ).dt.tz_localize(tz).astype(str), + 'decimal': list(map(decimal.Decimal, ['1.0', '2', '3.234'])), }) @@ -66,7 +72,7 @@ def client(df, df1, df2): @pytest.fixture def t(client): - return client.table('df') + return client.table('df', schema={'decimal': dt.Decimal(4, 3)}) @pytest.fixture @@ -89,6 +95,11 @@ def test_literal(client): assert client.execute(ibis.literal(1)) == 1 +def test_read_with_undiscoverable_type(client): + with pytest.raises(TypeError): + client.table('df') + + @pytest.mark.parametrize('from_', ['plain_float64', 'plain_int64']) @pytest.mark.parametrize( ('to', 'expected'), @@ -184,10 +195,8 @@ def test_cast_date(t, df): (lambda v: v.second(), lambda vt: vt.second), (lambda v: v.millisecond(), lambda vt: int(vt.microsecond / 1e3)), ] + [ - ( - operator.methodcaller('strftime', pattern), - operator.methodcaller('strftime', pattern), - ) for pattern in [ + (methodcaller('strftime', pattern), methodcaller('strftime', pattern)) + for pattern in [ '%Y%m%d %H', 'DD BAR %w FOO "DD"', 'DD BAR %w FOO "D', @@ -762,3 +771,136 @@ def test_cast_integer_to_date(t, df): name='plain_int64', ) tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize('places', [-2, 0, 1, 2, None]) +def test_round(t, df, places): + expr = t.float64_as_strings.cast('double').round(places) + result = expr.execute() + expected = t.execute().float64_as_strings.astype('float64').round( + places if places is not None else 0 + ) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + ('ibis_func', 'pandas_func'), + [ + (methodcaller('round'), methodcaller('round')), + (methodcaller('round', 2), methodcaller('round', 2)), + (methodcaller('round', -2), methodcaller('round', -2)), + (methodcaller('round', 0), methodcaller('round', 0)), + (methodcaller('ceil'), np.ceil), + (methodcaller('floor'), np.floor), + (methodcaller('exp'), np.exp), + (methodcaller('sign'), np.sign), + (methodcaller('sqrt'), np.sqrt), + (methodcaller('log', 2), lambda x: np.log(x) / np.log(2)), + (methodcaller('ln'), np.log), + (methodcaller('log2'), np.log2), + (methodcaller('log10'), np.log10), + ] +) +def test_math_functions(t, df, ibis_func, pandas_func): + result = ibis_func(t.float64_with_zeros).execute() + expected = pandas_func(df.float64_with_zeros) + tm.assert_series_equal(result, expected) + + +def operate(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except decimal.InvalidOperation: + return decimal.Decimal('NaN') + return wrapper + + +@pytest.mark.parametrize( + ('ibis_func', 'pandas_func'), + [ + (methodcaller('round'), lambda x: np.int64(round(x))), + ( + methodcaller('round', 2), + lambda x: x.quantize(decimal.Decimal('.00')) + ), + ( + methodcaller('round', 0), + lambda x: x.quantize(decimal.Decimal('0.')) + ), + (methodcaller('ceil'), lambda x: decimal.Decimal(math.ceil(x))), + (methodcaller('floor'), lambda x: decimal.Decimal(math.floor(x))), + (methodcaller('exp'), methodcaller('exp')), + ( + methodcaller('sign'), + lambda x: x if not x else decimal.Decimal(1).copy_sign(x) + ), + (methodcaller('sqrt'), operate(lambda x: x.sqrt())), + ( + methodcaller('log', 2), + operate(lambda x: x.ln() / decimal.Decimal(2).ln()) + ), + (methodcaller('ln'), operate(lambda x: x.ln())), + ( + methodcaller('log2'), + operate(lambda x: x.ln() / decimal.Decimal(2).ln()) + ), + (methodcaller('log10'), operate(lambda x: x.log10())), + ] +) +def test_math_functions_decimal(t, df, ibis_func, pandas_func): + type = dt.Decimal(12, 3) + result = ibis_func(t.float64_as_strings.cast(type)).execute() + context = decimal.Context(prec=type.precision) + expected = df.float64_as_strings.apply( + lambda x: context.create_decimal(x).quantize( + decimal.Decimal( + '{}.{}'.format( + '0' * (type.precision - type.scale), + '0' * type.scale + ) + ) + ) + ).apply(pandas_func) + + result[result.apply(math.isnan)] = -99999 + expected[expected.apply(math.isnan)] = -99999 + tm.assert_series_equal(result, expected) + + +def test_round_decimal_with_negative_places(t, df): + type = dt.Decimal(12, 3) + expr = t.float64_as_strings.cast(type).round(-1) + result = expr.execute() + expected = pd.Series( + list(map(decimal.Decimal, ['1.0E+2', '2.3E+2', '-1.00E+3'])), + name='float64_as_strings' + ) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize('type', [dt.Decimal(9, 0), dt.Decimal(12, 3)]) +def test_cast_to_decimal(t, df, type): + expr = t.float64_as_strings.cast(type) + result = expr.execute() + context = decimal.Context(prec=type.precision) + expected = df.float64_as_strings.apply( + lambda x: context.create_decimal(x).quantize( + decimal.Decimal( + '{}.{}'.format( + '0' * (type.precision - type.scale), + '0' * type.scale + ) + ) + ) + ) + tm.assert_series_equal(result, expected) + assert all( + abs(element.as_tuple().exponent) == type.scale + for element in result.values + ) + assert all( + 1 <= len(element.as_tuple().digits) <= type.precision + for element in result.values + ) diff --git a/ibis/sql/postgres/compiler.py b/ibis/sql/postgres/compiler.py index a2fe6bd70f5e..80f227ec8f76 100644 --- a/ibis/sql/postgres/compiler.py +++ b/ibis/sql/postgres/compiler.py @@ -536,6 +536,43 @@ def _table_column(t, expr): return out_expr +def _round(t, expr): + arg, digits = expr.op().args + sa_arg = t.translate(arg) + + if digits is None: + return sa.func.round(sa_arg) + + # postgres doesn't allow rounding of double precision values to a specific + # number of digits (though simple truncation on doubles is allowed) so + # we cast to numeric and then cast back if necessary + result = sa.func.round(sa.cast(sa_arg, sa.NUMERIC), t.translate(digits)) + if digits is not None and isinstance(arg.type(), dt.Decimal): + return result + return sa.cast(result, sa.dialects.postgresql.DOUBLE_PRECISION()) + + +def _mod(t, expr): + left, right = map(t.translate, expr.op().args) + + # postgres doesn't allow modulus of double precision values, so upcast and + # then downcast later if necessary + if not isinstance(expr.type(), dt.Integer): + left = sa.cast(left, sa.NUMERIC) + right = sa.cast(right, sa.NUMERIC) + + result = left % right + if expr.type().equals(dt.double): + return sa.cast(result, sa.dialects.postgresql.DOUBLE_PRECISION()) + else: + return result + + +def _floor_divide(t, expr): + left, right = map(t.translate, expr.op().args) + return sa.func.floor(left / right) + + _operation_registry.update({ # We override this here to support time zones ops.TableColumn: _table_column, @@ -585,6 +622,7 @@ def _table_column(t, expr): ops.Ceil: fixed_arity(sa.func.ceil, 1), ops.Floor: fixed_arity(sa.func.floor, 1), + ops.FloorDivide: _floor_divide, ops.Exp: fixed_arity(sa.func.exp, 1), ops.Sign: fixed_arity(sa.func.sign, 1), ops.Sqrt: fixed_arity(sa.func.sqrt, 1), @@ -593,6 +631,8 @@ def _table_column(t, expr): ops.Log2: fixed_arity(lambda x: sa.func.log(2, x), 1), ops.Log10: fixed_arity(sa.func.log, 1), ops.Power: fixed_arity(sa.func.power, 2), + ops.Round: _round, + ops.Modulus: _mod, # dates and times ops.Strftime: _strftime, diff --git a/ibis/sql/sqlite/client.py b/ibis/sql/sqlite/client.py index 73ffc4645b4b..b0a9c3099145 100644 --- a/ibis/sql/sqlite/client.py +++ b/ibis/sql/sqlite/client.py @@ -34,6 +34,21 @@ class SQLiteDatabase(Database): pass +_SQLITE_UDF_REGISTRY = set() +_SQLITE_UDAF_REGISTRY = set() + + +def udf(f): + _SQLITE_UDF_REGISTRY.add(f) + return f + + +def udaf(f): + _SQLITE_UDAF_REGISTRY.add(f) + return f + + +@udf def _ibis_sqlite_regex_search(string, regex): """Return whether `regex` exists in `string`. @@ -51,6 +66,7 @@ def _ibis_sqlite_regex_search(string, regex): return re.search(regex, string) is not None +@udf def _ibis_sqlite_regex_replace(string, pattern, replacement): """Replace occurences of `pattern` in `string` with `replacement`. @@ -69,6 +85,7 @@ def _ibis_sqlite_regex_replace(string, pattern, replacement): return re.sub(pattern, replacement, string) +@udf def _ibis_sqlite_regex_extract(string, pattern, index): """Extract match of regular expression `pattern` from `string` at `index`. @@ -92,6 +109,73 @@ def _ibis_sqlite_regex_extract(string, pattern, index): return None +@udf +def _ibis_sqlite_exp(arg): + """Exponentiate `arg`. + + Parameters + ---------- + arg : number + Number to raise to `e`. + + Returns + ------- + result : Optional[number] + None If the input is None + """ + return math.exp(arg) if arg is not None else None + + +@udf +def _ibis_sqlite_log(arg, base): + if arg is None or base is None or arg < 0 or base < 0: + return None + return math.log(arg, base) + + +@udf +def _ibis_sqlite_ln(arg): + if arg is None or arg < 0: + return None + return math.log(arg) + + +@udf +def _ibis_sqlite_log2(arg): + return _ibis_sqlite_log(arg, 2) + + +@udf +def _ibis_sqlite_log10(arg): + return _ibis_sqlite_log(arg, 10) + + +@udf +def _ibis_sqlite_floor(arg): + return math.floor(arg) if arg is not None else None + + +@udf +def _ibis_sqlite_ceil(arg): + return math.ceil(arg) if arg is not None else None + + +@udf +def _ibis_sqlite_sign(arg): + if arg is None: + return None + elif arg == 0: + return 0 + else: + return math.copysign(1, arg) + + +@udf +def _ibis_sqlite_floordiv(left, right): + return left // right + + +@udf def _ibis_sqlite_power(arg, power): """Raise `arg` to the `power` power. @@ -113,6 +197,7 @@ def _ibis_sqlite_power(arg, power): return arg ** power +@udf def _ibis_sqlite_sqrt(arg): """Square root of `arg`. @@ -152,12 +237,14 @@ def finalize(self): return self.sum_of_squares_of_differences / (self.count - self.offset) +@udaf class _ibis_sqlite_var_pop(_ibis_sqlite_var): def __init__(self): super(_ibis_sqlite_var_pop, self).__init__(0) +@udaf class _ibis_sqlite_var_samp(_ibis_sqlite_var): def __init__(self): @@ -228,16 +315,10 @@ def __init__(self, path=None, create=False): if path is not None: self.attach(self.database_name, path, create=create) - for func in ( - _ibis_sqlite_regex_search, - _ibis_sqlite_regex_replace, - _ibis_sqlite_regex_extract, - _ibis_sqlite_power, - _ibis_sqlite_sqrt, - ): + for func in _SQLITE_UDF_REGISTRY: self.con.run_callable(functools.partial(_register_function, func)) - for agg in (_ibis_sqlite_var_pop, _ibis_sqlite_var_samp): + for agg in _SQLITE_UDAF_REGISTRY: self.con.run_callable(functools.partial(_register_aggregate, agg)) @property diff --git a/ibis/sql/sqlite/compiler.py b/ibis/sql/sqlite/compiler.py index 07100eb9c0c2..bcdfd049c5b1 100644 --- a/ibis/sql/sqlite/compiler.py +++ b/ibis/sql/sqlite/compiler.py @@ -144,6 +144,14 @@ def _identical_to(t, expr): ) +def _log(t, expr): + arg, base = expr.op().args + sa_arg = t.translate(arg) + if base is None: + return sa.func._ibis_sqlite_ln(sa_arg) + return sa.func._ibis_sqlite_log(sa_arg, t.translate(base)) + + _operation_registry.update({ ops.Cast: _cast, @@ -179,11 +187,23 @@ def _identical_to(t, expr): ops.ExtractMillisecond: _millisecond, ops.TimestampNow: _now, ops.IdenticalTo: _identical_to, + ops.RegexSearch: fixed_arity(sa.func._ibis_sqlite_regex_search, 2), ops.RegexReplace: fixed_arity(sa.func._ibis_sqlite_regex_replace, 3), ops.RegexExtract: fixed_arity(sa.func._ibis_sqlite_regex_extract, 3), + ops.Sqrt: fixed_arity(sa.func._ibis_sqlite_sqrt, 1), ops.Power: fixed_arity(sa.func._ibis_sqlite_power, 2), + ops.Exp: fixed_arity(sa.func._ibis_sqlite_exp, 1), + ops.Ln: fixed_arity(sa.func._ibis_sqlite_ln, 1), + ops.Log: _log, + ops.Log10: fixed_arity(sa.func._ibis_sqlite_log10, 1), + ops.Log2: fixed_arity(sa.func._ibis_sqlite_log2, 1), + ops.Floor: fixed_arity(sa.func._ibis_sqlite_floor, 1), + ops.Ceil: fixed_arity(sa.func._ibis_sqlite_ceil, 1), + ops.Sign: fixed_arity(sa.func._ibis_sqlite_sign, 1), + ops.FloorDivide: fixed_arity(sa.func._ibis_sqlite_floordiv, 2), + ops.Variance: _variance_reduction('_ibis_sqlite_var'), ops.StandardDev: toolz.compose( sa.func._ibis_sqlite_sqrt, diff --git a/ibis/sql/sqlite/tests/test_functions.py b/ibis/sql/sqlite/tests/test_functions.py index 40b312163214..0235161f3dcc 100644 --- a/ibis/sql/sqlite/tests/test_functions.py +++ b/ibis/sql/sqlite/tests/test_functions.py @@ -231,6 +231,17 @@ def test_math_functions(self): (L(5.5).round(), 6.0), (L(5.556).round(2), 5.56), (L(5.556).sqrt(), math.sqrt(5.556)), + + (L(5.556).ceil(), 6.0), + (L(5.556).floor(), 5.0), + (L(5.556).exp(), math.exp(5.556)), + (L(5.556).sign(), 1), + (L(-5.556).sign(), -1), + (L(0).sign(), 0), + (L(5.556).log(2), math.log(5.556, 2)), + (L(5.556).ln(), math.log(5.556)), + (L(5.556).log2(), math.log(5.556, 2)), + (L(5.556).log10(), math.log10(5.556)), ] self._check_e2e_cases(cases)