6666# Standard Substrait function-extension URNs used by the operators below.
6767FUNCTIONS_COMPARISON = "extension:io.substrait:functions_comparison"
6868FUNCTIONS_ARITHMETIC = "extension:io.substrait:functions_arithmetic"
69+ FUNCTIONS_ARITHMETIC_DECIMAL = "extension:io.substrait:functions_arithmetic_decimal"
6970FUNCTIONS_BOOLEAN = "extension:io.substrait:functions_boolean"
7071FUNCTIONS_STRING = "extension:io.substrait:functions_string"
7172FUNCTIONS_AGGREGATE_GENERIC = "extension:io.substrait:functions_aggregate_generic"
7273FUNCTIONS_LIST = "extension:io.substrait:functions_list"
7374
75+ # Arithmetic operators try the base extension first, then its decimal variant, so
76+ # decimal operands resolve against ``functions_arithmetic_decimal`` (there is no
77+ # such fallback for comparisons -- ``functions_comparison`` is generic ``any1``).
78+ _ARITHMETIC_URNS = [FUNCTIONS_ARITHMETIC , FUNCTIONS_ARITHMETIC_DECIMAL ]
79+
80+
81+ # Substrait bounds a decimal's precision to 38 (a 128-bit unscaled value).
82+ _MAX_DECIMAL_PRECISION = 38
83+
7484
7585def _decimal_type (value : _Decimal ) -> stp .Type :
7686 exponent = value .as_tuple ().exponent
@@ -83,6 +93,12 @@ def _decimal_type(value: _Decimal) -> stp.Type:
8393 # too small for the encoded value.
8494 digits = len (value .as_tuple ().digits ) + max (exponent , 0 )
8595 precision = max (digits , scale , 1 )
96+ if precision > _MAX_DECIMAL_PRECISION :
97+ raise ValueError (
98+ f"decimal literal { value !r} needs precision { precision } , exceeding "
99+ f"Substrait's maximum decimal precision of { _MAX_DECIMAL_PRECISION } ; "
100+ f"the value has too many significant digits for a decimal"
101+ )
86102 return _t .decimal (scale , precision )
87103
88104
@@ -135,31 +151,134 @@ def infer_literal_type(value: Any) -> stp.Type:
135151}
136152
137153
138- def _match_numeric_type (peer_type : stp .Type , value : Any ) -> stp .Type :
154+ def _exact_unscaled (value : _Decimal , scale : int ) -> Union [int , None ]:
155+ """The unscaled integer of ``value`` at ``scale``, or ``None`` if representing
156+ it at ``scale`` would drop nonzero digits (i.e. the value needs a finer scale).
157+
158+ Pure-integer arithmetic on ``as_tuple()`` so it is exact regardless of the
159+ ``decimal`` context precision.
160+ """
161+ sign , digits , exponent = value .as_tuple ()
162+ coeff = 0
163+ for d in digits :
164+ coeff = coeff * 10 + d
165+ shift = exponent + scale
166+ if shift < 0 : # value carries more fractional digits than `scale` allows
167+ factor = 10 ** (- shift )
168+ if coeff % factor :
169+ return None
170+ coeff //= factor
171+ else :
172+ coeff *= 10 ** shift
173+ return - coeff if sign else coeff
174+
175+
176+ def _match_numeric_type (
177+ peer_type : stp .Type , value : Any , * , decimal_literal : str = "natural"
178+ ) -> stp .Type :
139179 """Pick a literal type for ``value`` that matches a numeric ``peer_type``.
140180
141181 Substrait does not implicitly coerce mixed numeric operands, so
142182 ``col("price_fp64") * 2`` needs the ``2`` typed as ``fp64`` rather than the
143183 default ``i64`` for the ``multiply`` overload to resolve. A ``float`` value
144184 always stays floating point to avoid a lossy narrowing.
185+
186+ For a ``decimal`` peer the coercion depends on the operation, selected by
187+ ``decimal_literal``:
188+
189+ - ``"peer"`` (comparisons) -- the ``functions_comparison`` overloads use
190+ ``any1, any1`` and so require *identical* operand types, so the literal must
191+ take the column's exact ``decimal(precision, scale)``. That is only sound
192+ when the value is *exactly* representable there; if it would need a finer
193+ scale or overflow the precision this raises rather than silently rounding or
194+ truncating the comparison, so the user coerces explicitly with
195+ ``lit(value, <decimal type>)`` or by casting the column.
196+ - ``"natural"`` (arithmetic) -- ``functions_arithmetic_decimal`` accepts
197+ ``decimal<P1,S1>, decimal<P2,S2>`` and derives the result type, so the
198+ literal keeps its own natural decimal type (lossless).
199+
200+ A ``float`` against a decimal peer still stays floating point (so it raises
201+ rather than being silently turned into a decimal); pass a ``Decimal``.
145202 """
146203 kind = peer_type .WhichOneof ("kind" )
147204 if isinstance (value , float ):
148205 return _t .fp32 () if kind == "fp32" else _t .fp64 ()
206+ if kind == "decimal" :
207+ if decimal_literal == "peer" :
208+ scale = peer_type .decimal .scale
209+ precision = peer_type .decimal .precision
210+ dec = value if isinstance (value , _Decimal ) else _Decimal (value )
211+ if not dec .is_finite ():
212+ raise TypeError ("cannot compare against a non-finite Decimal literal" )
213+ column = f"decimal(precision={ precision } , scale={ scale } )"
214+ unscaled = _exact_unscaled (dec , scale )
215+ if unscaled is None :
216+ raise ValueError (
217+ f"decimal literal { value !r} has a finer scale than the { column } "
218+ f"column it is compared to; comparing them would round it. Wrap "
219+ f"it with lit(value, <decimal type>) or cast the column."
220+ )
221+ if abs (unscaled ) >= 10 ** precision :
222+ raise ValueError (
223+ f"decimal literal { value !r} does not fit the { column } column it "
224+ f"is compared to. Wrap it with lit(value, <decimal type>) or cast "
225+ f"the column."
226+ )
227+ return _t .decimal (scale , precision )
228+ return _decimal_type (value if isinstance (value , _Decimal ) else _Decimal (value ))
149229 builder = _NUMERIC_BUILDERS .get (kind )
150230 return builder () if builder else _t .i64 ()
151231
152232
233+ def _resolve_over_urns (
234+ builder , urns , name , bound , base_schema , registry , * , alias = None , options = None
235+ ):
236+ """Resolve ``name`` over ``urns`` (in order) against the bound operands' types.
237+
238+ Builds with the first URN whose overload matches the operands' signature and
239+ raises a uniform error if none do. Shared by the operator path
240+ (:func:`_numeric_binary`) and the ``f.*`` namespace's multi-URN helper
241+ (``substrait.dataframe.functions._multi_urn_helper``) so both resolve
242+ identically and their error text cannot drift apart.
243+ """
244+ signature = [
245+ typ for b in bound for typ in infer_extended_expression_schema (b ).types
246+ ]
247+ for urn in urns :
248+ if registry .lookup_function (urn , name , signature ):
249+ return builder (urn , name , expressions = bound , alias = alias , options = options )(
250+ base_schema , registry
251+ )
252+ kinds = [t .WhichOneof ("kind" ) for t in signature ]
253+ raise Exception (
254+ f"No matching overload for '{ name } ' across { urns } with signature { kinds } "
255+ )
256+
257+
153258def _numeric_binary (
154- self_expr : "Expr" , other : Any , urn : str , fn : str , * , swap : bool = False
259+ self_expr : "Expr" ,
260+ other : Any ,
261+ urns : Union [str , list ],
262+ fn : str ,
263+ * ,
264+ swap : bool = False ,
265+ decimal_literal : str = "natural" ,
155266) -> "Expr" :
156267 """Build a binary comparison/arithmetic expression with literal coercion.
157268
158269 A bare Python number is typed to match the *other* (column) operand at
159270 resolve time, so mixed-width numeric comparisons and arithmetic resolve
160271 against the standard extension overloads. ``swap`` handles reflected
161272 operators (e.g. ``100 - col("a")``), keeping operand order intact.
273+
274+ ``urns`` may be a single URN or a list tried in order: arithmetic passes
275+ ``[functions_arithmetic, functions_arithmetic_decimal]`` so decimal operands
276+ fall through to the decimal extension when the base one has no overload,
277+ mirroring the ``f.*`` namespace's multi-URN resolution. ``decimal_literal``
278+ (``"natural"`` / ``"peer"``) is forwarded to :func:`_match_numeric_type` to
279+ pick how a decimal literal is coerced against a decimal peer.
162280 """
281+ urns = [urns ] if isinstance (urns , str ) else list (urns )
163282 left_operand = other if swap else self_expr
164283 right_operand = self_expr if swap else other
165284
@@ -181,15 +300,23 @@ def bind(operand):
181300 def as_bound (value , is_expr ):
182301 if is_expr :
183302 return value
184- if not isinstance (value , bool ) and isinstance (value , (int , float )) and peer :
185- lit_type = _match_numeric_type (peer , value )
186- return literal (value , lit_type )(base_schema , registry )
303+ if peer is not None and not isinstance (value , bool ):
304+ # int/float coerce to match any numeric peer; a Decimal only when
305+ # the peer is itself decimal (else keep its natural decimal type).
306+ peer_is_decimal = peer .WhichOneof ("kind" ) == "decimal"
307+ if isinstance (value , (int , float )) or (
308+ isinstance (value , _Decimal ) and peer_is_decimal
309+ ):
310+ lit_type = _match_numeric_type (
311+ peer , value , decimal_literal = decimal_literal
312+ )
313+ return literal (value , lit_type )(base_schema , registry )
187314 return Expr ._coerce (value )._unbound (base_schema , registry )
188315
189316 left_bound = as_bound (left_val , left_is_expr )
190317 right_bound = as_bound (right_val , right_is_expr )
191- return scalar_function ( urn , fn , expressions = [ left_bound , right_bound ]) (
192- base_schema , registry
318+ return _resolve_over_urns (
319+ scalar_function , urns , fn , [ left_bound , right_bound ], base_schema , registry
193320 )
194321
195322 return Expr (resolve )
@@ -296,7 +423,9 @@ def _compare(self, other: Any, fn: str) -> "Expr":
296423 self ._unbound , other .plan , other .reduction_op , _COMPARISON_OPS [fn ]
297424 )
298425 )
299- return _numeric_binary (self , other , FUNCTIONS_COMPARISON , fn )
426+ return _numeric_binary (
427+ self , other , FUNCTIONS_COMPARISON , fn , decimal_literal = "peer"
428+ )
300429
301430 def __lt__ (self , other : Any ) -> "Expr" :
302431 return self ._compare (other , "lt" )
@@ -322,40 +451,40 @@ def __ne__(self, other: Any) -> "Expr": # type: ignore[override]
322451
323452 # -- arithmetic -------------------------------------------------------
324453 def __add__ (self , other : Any ) -> "Expr" :
325- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "add" )
454+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "add" )
326455
327456 def __sub__ (self , other : Any ) -> "Expr" :
328- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "subtract" )
457+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "subtract" )
329458
330459 def __mul__ (self , other : Any ) -> "Expr" :
331- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "multiply" )
460+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "multiply" )
332461
333462 def __truediv__ (self , other : Any ) -> "Expr" :
334- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "divide" )
463+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "divide" )
335464
336465 def __radd__ (self , other : Any ) -> "Expr" :
337- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "add" , swap = True )
466+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "add" , swap = True )
338467
339468 def __rsub__ (self , other : Any ) -> "Expr" :
340- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "subtract" , swap = True )
469+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "subtract" , swap = True )
341470
342471 def __rmul__ (self , other : Any ) -> "Expr" :
343- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "multiply" , swap = True )
472+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "multiply" , swap = True )
344473
345474 def __rtruediv__ (self , other : Any ) -> "Expr" :
346- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "divide" , swap = True )
475+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "divide" , swap = True )
347476
348477 def __mod__ (self , other : Any ) -> "Expr" :
349- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "modulus" )
478+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "modulus" )
350479
351480 def __rmod__ (self , other : Any ) -> "Expr" :
352- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "modulus" , swap = True )
481+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "modulus" , swap = True )
353482
354483 def __pow__ (self , other : Any ) -> "Expr" :
355- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "power" )
484+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "power" )
356485
357486 def __rpow__ (self , other : Any ) -> "Expr" :
358- return _numeric_binary (self , other , FUNCTIONS_ARITHMETIC , "power" , swap = True )
487+ return _numeric_binary (self , other , _ARITHMETIC_URNS , "power" , swap = True )
359488
360489 def __neg__ (self ) -> "Expr" :
361490 return Expr (
0 commit comments