Plain is headed towards 1.0! Subscribe for development updates →

  1from plain.models.expressions import Func, Value
  2from plain.models.fields import FloatField, IntegerField
  3from plain.models.functions import Cast
  4from plain.models.functions.mixins import (
  5    FixDecimalInputMixin,
  6    NumericOutputFieldMixin,
  7)
  8from plain.models.lookups import Transform
  9
 10
 11class Abs(Transform):
 12    function = "ABS"
 13    lookup_name = "abs"
 14
 15
 16class ACos(NumericOutputFieldMixin, Transform):
 17    function = "ACOS"
 18    lookup_name = "acos"
 19
 20
 21class ASin(NumericOutputFieldMixin, Transform):
 22    function = "ASIN"
 23    lookup_name = "asin"
 24
 25
 26class ATan(NumericOutputFieldMixin, Transform):
 27    function = "ATAN"
 28    lookup_name = "atan"
 29
 30
 31class ATan2(NumericOutputFieldMixin, Func):
 32    function = "ATAN2"
 33    arity = 2
 34
 35    def as_sqlite(self, compiler, connection, **extra_context):
 36        if not getattr(
 37            connection.ops, "spatialite", False
 38        ) or connection.ops.spatial_version >= (5, 0, 0):
 39            return self.as_sql(compiler, connection)
 40        # This function is usually ATan2(y, x), returning the inverse tangent
 41        # of y / x, but it's ATan2(x, y) on SpatiaLite < 5.0.0.
 42        # Cast integers to float to avoid inconsistent/buggy behavior if the
 43        # arguments are mixed between integer and float or decimal.
 44        # https://www.gaia-gis.it/fossil/libspatialite/tktview?name=0f72cca3a2
 45        clone = self.copy()
 46        clone.set_source_expressions(
 47            [
 48                Cast(expression, FloatField())
 49                if isinstance(expression.output_field, IntegerField)
 50                else expression
 51                for expression in self.get_source_expressions()[::-1]
 52            ]
 53        )
 54        return clone.as_sql(compiler, connection, **extra_context)
 55
 56
 57class Ceil(Transform):
 58    function = "CEILING"
 59    lookup_name = "ceil"
 60
 61
 62class Cos(NumericOutputFieldMixin, Transform):
 63    function = "COS"
 64    lookup_name = "cos"
 65
 66
 67class Cot(NumericOutputFieldMixin, Transform):
 68    function = "COT"
 69    lookup_name = "cot"
 70
 71
 72class Degrees(NumericOutputFieldMixin, Transform):
 73    function = "DEGREES"
 74    lookup_name = "degrees"
 75
 76
 77class Exp(NumericOutputFieldMixin, Transform):
 78    function = "EXP"
 79    lookup_name = "exp"
 80
 81
 82class Floor(Transform):
 83    function = "FLOOR"
 84    lookup_name = "floor"
 85
 86
 87class Ln(NumericOutputFieldMixin, Transform):
 88    function = "LN"
 89    lookup_name = "ln"
 90
 91
 92class Log(FixDecimalInputMixin, NumericOutputFieldMixin, Func):
 93    function = "LOG"
 94    arity = 2
 95
 96    def as_sqlite(self, compiler, connection, **extra_context):
 97        if not getattr(connection.ops, "spatialite", False):
 98            return self.as_sql(compiler, connection)
 99        # This function is usually Log(b, x) returning the logarithm of x to
100        # the base b, but on SpatiaLite it's Log(x, b).
101        clone = self.copy()
102        clone.set_source_expressions(self.get_source_expressions()[::-1])
103        return clone.as_sql(compiler, connection, **extra_context)
104
105
106class Mod(FixDecimalInputMixin, NumericOutputFieldMixin, Func):
107    function = "MOD"
108    arity = 2
109
110
111class Pi(NumericOutputFieldMixin, Func):
112    function = "PI"
113    arity = 0
114
115
116class Power(NumericOutputFieldMixin, Func):
117    function = "POWER"
118    arity = 2
119
120
121class Radians(NumericOutputFieldMixin, Transform):
122    function = "RADIANS"
123    lookup_name = "radians"
124
125
126class Random(NumericOutputFieldMixin, Func):
127    function = "RANDOM"
128    arity = 0
129
130    def as_mysql(self, compiler, connection, **extra_context):
131        return super().as_sql(compiler, connection, function="RAND", **extra_context)
132
133    def as_sqlite(self, compiler, connection, **extra_context):
134        return super().as_sql(compiler, connection, function="RAND", **extra_context)
135
136    def get_group_by_cols(self):
137        return []
138
139
140class Round(FixDecimalInputMixin, Transform):
141    function = "ROUND"
142    lookup_name = "round"
143    arity = None  # Override Transform's arity=1 to enable passing precision.
144
145    def __init__(self, expression, precision=0, **extra):
146        super().__init__(expression, precision, **extra)
147
148    def as_sqlite(self, compiler, connection, **extra_context):
149        precision = self.get_source_expressions()[1]
150        if isinstance(precision, Value) and precision.value < 0:
151            raise ValueError("SQLite does not support negative precision.")
152        return super().as_sqlite(compiler, connection, **extra_context)
153
154    def _resolve_output_field(self):
155        source = self.get_source_expressions()[0]
156        return source.output_field
157
158
159class Sign(Transform):
160    function = "SIGN"
161    lookup_name = "sign"
162
163
164class Sin(NumericOutputFieldMixin, Transform):
165    function = "SIN"
166    lookup_name = "sin"
167
168
169class Sqrt(NumericOutputFieldMixin, Transform):
170    function = "SQRT"
171    lookup_name = "sqrt"
172
173
174class Tan(NumericOutputFieldMixin, Transform):
175    function = "TAN"
176    lookup_name = "tan"