Repository: Azure/pykusto

Issue:
multiplying numbers and columns doesn't always work
examples:
Query().where(f.to_double(100 * t.numberField) > 0.2).render()
returns: TypeError: unsupported operand type(s) for *: 'int' and 'AnyTypeColumn'

Query().where(100 * f.to_double(t.numberField) > 0.2).render()
returns: TypeError: unsupported operand type(s) for *: 'int' and 'NumberExpression'

Query().where(col.of(100) * f.to_double(t.numberField) > 0.2).render()
renders as : "| where (['100'] * (todouble(numberField))) > 0.2" which doesn't compile.

I didn't have any more ideas...

Discussion:

Patch:
diff --git a/pykusto/expressions.py b/pykusto/expressions.py
index 70e8207..2dc2e4f 100644
--- a/pykusto/expressions.py
+++ b/pykusto/expressions.py
@@ -188,12 +188,24 @@ class BooleanExpression(BaseExpression):
         """
         return BooleanExpression.binary_op(self, ' and ', other)
 
+    def __rand__(self, other: BooleanType) -> 'BooleanExpression':
+        """
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/logicaloperators
+        """
+        return BooleanExpression.binary_op(other, ' and ', self)
+
     def __or__(self, other: BooleanType) -> 'BooleanExpression':
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/logicaloperators
         """
         return BooleanExpression.binary_op(self, ' or ', other)
 
+    def __ror__(self, other: BooleanType) -> 'BooleanExpression':
+        """
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/logicaloperators
+        """
+        return BooleanExpression.binary_op(other, ' or ', self)
+
     def __invert__(self) -> 'BooleanExpression':
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/notfunction
@@ -223,18 +235,33 @@ class NumberExpression(BaseExpression):
     def __add__(self, other: NumberType) -> 'NumberExpression':
         return NumberExpression.binary_op(self, ' + ', other)
 
+    def __radd__(self, other: NumberType) -> 'NumberExpression':
+        return NumberExpression.binary_op(other, ' + ', self)
+
     def __sub__(self, other: NumberType) -> 'NumberExpression':
         return NumberExpression.binary_op(self, ' - ', other)
 
+    def __rsub__(self, other: NumberType) -> 'NumberExpression':
+        return NumberExpression.binary_op(other, ' - ', self)
+
     def __mul__(self, other: NumberType) -> 'NumberExpression':
         return NumberExpression.binary_op(self, ' * ', other)
 
+    def __rmul__(self, other: NumberType) -> 'NumberExpression':
+        return NumberExpression.binary_op(other, ' * ', self)
+
     def __truediv__(self, other: NumberType) -> 'NumberExpression':
         return NumberExpression.binary_op(self, ' / ', other)
 
+    def __rtruediv__(self, other: NumberType) -> 'NumberExpression':
+        return NumberExpression.binary_op(other, ' / ', self)
+
     def __mod__(self, other: NumberType) -> 'NumberExpression':
         return NumberExpression.binary_op(self, ' % ', other)
 
+    def __rmod__(self, other: NumberType) -> 'NumberExpression':
+        return NumberExpression.binary_op(other, ' % ', self)
+
     def __neg__(self) -> 'NumberExpression':
         return NumberExpression(KQL(f'-{self.kql}'))
 
@@ -451,9 +478,12 @@ class DatetimeExpression(BaseExpression):
         return BooleanExpression.binary_op(self, ' >= ', other)
 
     def __add__(self, other: TimespanType) -> 'DatetimeExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
         return DatetimeExpression.binary_op(self, ' + ', other)
 
     def __sub__(self, other: Union[DatetimeType, TimespanType]) -> Union['DatetimeExpression', 'TimespanExpression']:
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
+
         # noinspection PyTypeChecker
         base_types = get_base_types(other)
         possible_types = base_types & {KustoType.DATETIME, KustoType.TIMESPAN}
@@ -464,6 +494,10 @@ class DatetimeExpression(BaseExpression):
             return_type = TimespanExpression if next(iter(possible_types)) == KustoType.DATETIME else DatetimeExpression
         return return_type(DatetimeExpression.binary_op(self, ' - ', other))
 
+    def __rsub__(self, other: DatetimeType) -> 'TimespanExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
+        return TimespanExpression.binary_op(other, ' - ', self)
+
     def between(self, lower: DatetimeType, upper: DatetimeType) -> BooleanExpression:
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/betweenoperator
@@ -591,11 +625,21 @@ class TimespanExpression(BaseExpression):
         return BaseExpression.base_binary_op(left, operator, right, KustoType.TIMESPAN)
 
     def __add__(self, other: TimespanType) -> 'TimespanExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
         return TimespanExpression.binary_op(self, ' + ', other)
 
+    def __radd__(self, other: TimespanType) -> 'TimespanExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
+        return TimespanExpression.binary_op(other, ' + ', self)
+
     def __sub__(self, other: TimespanType) -> 'TimespanExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
         return TimespanExpression.binary_op(self, ' - ', other)
 
+    def __rsub__(self, other: TimespanType) -> 'TimespanExpression':
+        # https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datetime-timespan-arithmetic
+        return TimespanExpression.binary_op(other, ' - ', self)
+
     def ago(self) -> DatetimeExpression:
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/agofunction
@@ -788,7 +832,9 @@ class BaseColumn(BaseExpression):
         return object.__new__(cls)
 
     def __init__(self, name: str, quote: bool = False) -> None:
-        super().__init__(KQL(f"['{name}']" if quote or '.' in name or name in KUSTO_KEYWORDS else name))
+        assert len(name) > 0, "Column name must not be empty"
+        should_quote = quote or '.' in name or name in KUSTO_KEYWORDS or name.isdigit()
+        super().__init__(KQL(f"['{name}']" if should_quote else name))
         self._name = name
 
     def get_name(self) -> str:
@@ -847,20 +893,40 @@ class TimespanColumn(BaseColumn, TimespanExpression):
 
 
 class SubtractableColumn(NumberColumn, DatetimeColumn, TimespanColumn):
-    def __sub__(self, other: Union['NumberType', 'DatetimeType', 'TimespanType']) -> Union['NumberExpression', 'TimespanExpression', 'AnyExpression']:
+    @staticmethod
+    def __resolve_type(type_to_resolve: Union['NumberType', 'DatetimeType', 'TimespanType']) -> Optional[KustoType]:
         # noinspection PyTypeChecker
-        base_types = get_base_types(other)
+        base_types = get_base_types(type_to_resolve)
         possible_types = base_types & ({KustoType.DATETIME, KustoType.TIMESPAN} | NUMBER_TYPES)
-
         assert len(possible_types) > 0, "Invalid type subtracted"
         if possible_types == NUMBER_TYPES:
-            return_type = KustoType.INT
-        elif len(possible_types) > 1:
-            return_type = None
-        else:
-            return_type = KustoType.TIMESPAN if next(iter(possible_types)) == KustoType.DATETIME else None
+            return KustoType.INT
+        if len(possible_types) > 1:
+            return None
+        return next(iter(possible_types))
+
+    def __sub__(self, other: Union['NumberType', 'DatetimeType', 'TimespanType']) -> Union['NumberExpression', 'TimespanExpression', 'AnyExpression']:
+        resolved_type = self.__resolve_type(other)
+        if resolved_type == KustoType.DATETIME:
+            # Subtracting a datetime can only result in a timespan
+            resolved_type = KustoType.TIMESPAN
+        elif resolved_type == KustoType.TIMESPAN:
+            # Subtracting a timespan can result in either a timespan or a datetime
+            resolved_type = None
+        # Otherwise: subtracting a number can only result in a number
+
+        # noinspection PyTypeChecker
+        return BaseExpression.base_binary_op(self, ' - ', other, resolved_type)
+
+    def __rsub__(self, other: Union['NumberType', 'DatetimeType', 'TimespanType']) -> Union['NumberExpression', 'TimespanExpression', 'AnyExpression']:
+        resolved_type = self.__resolve_type(other)
+        if resolved_type == KustoType.DATETIME:
+            # Subtracting from a datetime can result in either a datetime or a timespan
+            resolved_type = None
+        # Otherwise: subtracting from a number or a timespan can only result in a number or a timespan respectively
+
         # noinspection PyTypeChecker
-        return BaseExpression.base_binary_op(self, ' - ', other, return_type)
+        return BaseExpression.base_binary_op(other, ' - ', self, resolved_type)
 
 
 class AnyTypeColumn(SubtractableColumn, BooleanColumn, DynamicColumn, StringColumn):
diff --git a/pykusto/functions.py b/pykusto/functions.py
index 50f98ea..fdc4fb9 100644
--- a/pykusto/functions.py
+++ b/pykusto/functions.py
@@ -131,6 +131,7 @@ class Functions:
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/casefunction
         """
+        assert len(args) > 0, "case must have at least three arguments"
         return AnyExpression(KQL(f"case({to_kql(predicate)}, {to_kql(val)}, {', '.join(to_kql(arg) for arg in args)})"))
 
     @staticmethod


Test patch:
diff --git a/test/test_expressions.py b/test/test_expressions.py
index be7f396..0e47b49 100644
--- a/test/test_expressions.py
+++ b/test/test_expressions.py
@@ -1,6 +1,7 @@
 from datetime import timedelta, datetime
 
 from pykusto.expressions import column_generator as col, AnyTypeColumn
+from pykusto.functions import Functions as f
 from pykusto.query import Query
 from test.test_base import TestBase, test_table as t
 
@@ -78,12 +79,24 @@ class TestExpressions(TestBase):
             Query().where(t.boolField & t.stringField.contains("hello")).render(),
         )
 
+    def test_swapped_and(self):
+        self.assertEqual(
+            ' | where 1 and boolField',
+            Query().where(1 & t.boolField).render(),
+        )
+
     def test_or(self):
         self.assertEqual(
             ' | where boolField or (stringField contains "hello")',
             Query().where(t.boolField | t.stringField.contains("hello")).render(),
         )
 
+    def test_swapped_or(self):
+        self.assertEqual(
+            ' | where 0 or boolField',
+            Query().where(0 | t.boolField).render(),
+        )
+
     def test_not(self):
         self.assertEqual(
             ' | where not(stringField contains "hello")',
@@ -102,12 +115,24 @@ class TestExpressions(TestBase):
             Query().extend(foo=t.numField / 2).render(),
         )
 
+    def test_swapped_div(self):
+        self.assertEqual(
+            ' | extend foo = 2 / numField',
+            Query().extend(foo=2 / t.numField).render(),
+        )
+
     def test_mod(self):
         self.assertEqual(
             ' | extend foo = numField % 2',
             Query().extend(foo=t.numField % 2).render(),
         )
 
+    def test_swapped_mod(self):
+        self.assertEqual(
+            ' | extend foo = 2 % numField',
+            Query().extend(foo=2 % t.numField).render(),
+        )
+
     def test_negation(self):
         self.assertEqual(
             ' | extend foo = -numField',
@@ -192,12 +217,24 @@ class TestExpressions(TestBase):
             Query().extend(foo=t.timespanField + timedelta(hours=1)).render(),
         )
 
+    def test_add_swapped_timespan_to_timespan(self):
+        self.assertEqual(
+            ' | extend foo = time(0.1:0:0.0) + timespanField',
+            Query().extend(foo=timedelta(hours=1) + t.timespanField).render(),
+        )
+
     def test_subtract_timespan_from_timespan(self):
         self.assertEqual(
             ' | extend foo = timespanField - time(0.1:0:0.0)',
             Query().extend(foo=t.timespanField - timedelta(hours=1)).render(),
         )
 
+    def test_swapped_subtract_timespan_from_timespan(self):
+        self.assertEqual(
+            ' | extend foo = time(0.1:0:0.0) - timespanField',
+            Query().extend(foo=timedelta(hours=1) - t.timespanField).render(),
+        )
+
     def test_sub_timespan(self):
         self.assertEqual(
             ' | extend foo = dateField - time(0.1:0:0.0)',
@@ -210,7 +247,25 @@ class TestExpressions(TestBase):
             Query().extend(foo=t.dateField - datetime(2020, 1, 1)).render(),
         )
 
+    def test_sub_from_datetime(self):
+        self.assertEqual(
+            ' | extend foo = datetime(2020-01-01 00:00:00.000000) - dateField',
+            Query().extend(foo=datetime(2020, 1, 1) - t.dateField).render(),
+        )
+
+    def test_sub_from_number(self):
+        self.assertEqual(
+            ' | extend foo = 3 - numField',
+            Query().extend(foo=3 - t.numField).render(),
+        )
+
     def test_sub_date_unknown_type(self):
+        self.assertEqual(
+            ' | extend foo = dateField - (case(boolField, bar, baz))',
+            Query().extend(foo=t.dateField - f.case(t.boolField, col.bar, col.baz)).render(),
+        )
+
+    def test_sub_date_unknown_column(self):
         self.assertEqual(
             ' | extend foo = dateField - bar',
             Query().extend(foo=t.dateField - col.bar).render(),
@@ -359,3 +414,27 @@ class TestExpressions(TestBase):
             ' | where [\'stringField\'] has "test"',
             Query().where(col.of('stringField').has("test")).render()
         )
+
+    def test_multiply_number_column(self):
+        self.assertEqual(
+            ' | where (todouble(100 * numberField)) > 0.2',
+            Query().where(f.to_double(100 * t.numberField) > 0.2).render(),
+        )
+
+    def test_add_number_column(self):
+        self.assertEqual(
+            ' | where (todouble(100 + numberField)) > 0.2',
+            Query().where(f.to_double(100 + t.numberField) > 0.2).render(),
+        )
+
+    def test_multiply_number_expression(self):
+        self.assertEqual(
+            ' | where (100 * (todouble(numberField))) > 0.2',
+            Query().where(100 * f.to_double(t.numberField) > 0.2).render(),
+        )
+
+    def test_column_with_digits(self):
+        self.assertEqual(
+            " | where (['100'] * (todouble(numberField))) > 0.2",
+            Query().where(col['100'] * f.to_double(t.numberField) > 0.2).render(),
+        )