Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/functions/math.py: 81%

1 from plain.models.expressions import Func, Value

2 from plain.models.fields import FloatField, IntegerField

3 from plain.models.functions import Cast

4 from plain.models.functions.mixins import (

5 FixDecimalInputMixin,

6 NumericOutputFieldMixin,

7)

8 from plain.models.lookups import Transform

9

10

11 class Abs(Transform):

12 function = "ABS"

13 lookup_name = "abs"

14

15

16 class ACos(NumericOutputFieldMixin, Transform):

17 function = "ACOS"

18 lookup_name = "acos"

19

20

21 class ASin(NumericOutputFieldMixin, Transform):

22 function = "ASIN"

23 lookup_name = "asin"

24

25

26 class ATan(NumericOutputFieldMixin, Transform):

27 function = "ATAN"

28 lookup_name = "atan"

29

30

31 class 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

57 class Ceil(Transform):

58 function = "CEILING"

59 lookup_name = "ceil"

60

61

62 class Cos(NumericOutputFieldMixin, Transform):

63 function = "COS"

64 lookup_name = "cos"

65

66

67 class Cot(NumericOutputFieldMixin, Transform):

68 function = "COT"

69 lookup_name = "cot"

70

71

72 class Degrees(NumericOutputFieldMixin, Transform):

73 function = "DEGREES"

74 lookup_name = "degrees"

75

76

77 class Exp(NumericOutputFieldMixin, Transform):

78 function = "EXP"

79 lookup_name = "exp"

80

81

82 class Floor(Transform):

83 function = "FLOOR"

84 lookup_name = "floor"

85

86

87 class Ln(NumericOutputFieldMixin, Transform):

88 function = "LN"

89 lookup_name = "ln"

90

91

92 class 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

106 class Mod(FixDecimalInputMixin, NumericOutputFieldMixin, Func):

107 function = "MOD"

108 arity = 2

109

110

111 class Pi(NumericOutputFieldMixin, Func):

112 function = "PI"

113 arity = 0

114

115

116 class Power(NumericOutputFieldMixin, Func):

117 function = "POWER"

118 arity = 2

119

120

121 class Radians(NumericOutputFieldMixin, Transform):

122 function = "RADIANS"

123 lookup_name = "radians"

124

125

126 class 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

140 class 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

159 class Sign(Transform):

160 function = "SIGN"

161 lookup_name = "sign"

162

163

164 class Sin(NumericOutputFieldMixin, Transform):

165 function = "SIN"

166 lookup_name = "sin"

167

168

169 class Sqrt(NumericOutputFieldMixin, Transform):

170 function = "SQRT"

171 lookup_name = "sqrt"

172

173

174 class Tan(NumericOutputFieldMixin, Transform):

175 function = "TAN"

176 lookup_name = "tan"