Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions crates/polars-compute/src/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,55 @@ pub fn dec128_to_i128(x: i128, s: usize) -> i128 {
if s == 0 { x } else { div_128_pow10(x, s) }
}

/// Returns `x * 10^e`, or None on overflow; `e` is not bounded by `DEC128_MAX_PREC`.
#[inline]
pub fn i128_mul_pow10(x: i128, mut e: usize) -> Option<i128> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to constant fold decimal literals before we convert to float.

let mut r = x;
while e > DEC128_MAX_PREC {
r = r.checked_mul(POW10_I128[DEC128_MAX_PREC])?;
e -= DEC128_MAX_PREC;
}
r.checked_mul(POW10_I128[e])
}

/// Exact scalar arithmetic on `(mantissa, scale)` fixed-point values, bounded only by
/// `i128`: the result scale is the larger input scale for `add`/`sub` and the sum of the
/// input scales for `mul`, so no rounding takes place. Returns None on overflow.
pub mod exact {
use super::i128_mul_pow10;

fn align(l: (i128, usize), r: (i128, usize)) -> Option<(i128, i128, usize)> {
let s = l.1.max(r.1);
Some((
i128_mul_pow10(l.0, s - l.1)?,
i128_mul_pow10(r.0, s - r.1)?,
s,
))
}

#[inline]
pub fn add(l: (i128, usize), r: (i128, usize)) -> Option<(i128, usize)> {
let (l, r, s) = align(l, r)?;
Some((l.checked_add(r)?, s))
}

#[inline]
pub fn sub(l: (i128, usize), r: (i128, usize)) -> Option<(i128, usize)> {
let (l, r, s) = align(l, r)?;
Some((l.checked_sub(r)?, s))
}

#[inline]
pub fn mul(l: (i128, usize), r: (i128, usize)) -> Option<(i128, usize)> {
Some((l.0.checked_mul(r.0)?, l.1 + r.1))
}

#[inline]
pub fn neg(x: (i128, usize)) -> Option<(i128, usize)> {
Some((x.0.checked_neg()?, x.1))
}
}

/// Converts an i128 to a Decimal128 with the given precision and scale,
/// returning None if the value doesn't fit.
#[inline]
Expand Down Expand Up @@ -886,6 +935,29 @@ mod test {

use super::*;

#[test]
fn test_exact_scalar_arithmetic() {
// 0.06 + 0.01 = 0.07, exact at scale 2
assert_eq!(exact::add((6, 2), (1, 2)), Some((7, 2)));
// 1.5 - 3 aligns to the larger scale
assert_eq!(exact::sub((15, 1), (3, 0)), Some((-15, 1)));
// 1.10 * 1.10 = 1.2100 at the summed scale
assert_eq!(exact::mul((110, 2), (110, 2)), Some((12100, 4)));
assert_eq!(exact::neg((5, 1)), Some((-5, 1)));

// scales beyond DEC128_MAX_PREC stay exact
assert_eq!(exact::mul((1, 38), (1, 38)), Some((1, 76)));
assert_eq!(exact::add((0, 0), (1, 76)), Some((1, 76)));
assert_eq!(i128_mul_pow10(0, 200), Some(0));

// overflow is reported rather than wrapped
assert_eq!(exact::add((i128::MAX, 0), (1, 0)), None);
assert_eq!(exact::add((1, 0), (1, 39)), None);
assert_eq!(exact::mul((i128::MAX, 0), (2, 0)), None);
assert_eq!(exact::neg((i128::MIN, 0)), None);
assert_eq!(i128_mul_pow10(1, 39), None);
}

fn bigdecimal_to_dec128(x: &BigDecimal, p: usize, s: usize) -> Option<i128> {
let n = x
.with_scale_round(s as i64, RoundingMode::HalfEven)
Expand Down
7 changes: 7 additions & 0 deletions crates/polars-plan/src/dsl/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,11 @@ impl MetaNameSpace {
let ae = expr_arena.get(e_ir.node());
Ok(is_row_separable(&mut stack, ae, &expr_arena))
}

/// Indicate if this expression yields one scalar value that does not depend on any
/// input frame (see [`is_input_independent_scalar_rec`]).
pub fn is_input_independent_scalar(self) -> PolarsResult<bool> {
let (e_ir, expr_arena) = self.into_expr_ir()?;
Ok(is_input_independent_scalar_rec(e_ir.node(), &expr_arena))
}
}
19 changes: 19 additions & 0 deletions crates/polars-plan/src/plans/aexpr/properties/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,25 @@ pub fn is_elementwise_rec(node: Node, expr_arena: &Arena<AExpr>) -> bool {
property_rec(node, expr_arena, is_elementwise)
}

/// Whether `node` yields one scalar value that does not depend on any input frame: a scalar
/// literal, or elementwise operations (binary, cast, ternary, elementwise function) over such
/// values. Columns, the frame length, aggregations, windows, nested evaluations and user
/// functions are excluded.
pub fn is_input_independent_scalar_rec(node: Node, expr_arena: &Arena<AExpr>) -> bool {
property_rec(node, expr_arena, |stack, ae, _| {
let independent = match ae {
AExpr::Literal(lv) => lv.is_scalar(),
AExpr::BinaryExpr { .. } | AExpr::Cast { .. } | AExpr::Ternary { .. } => true,
AExpr::Function { options, .. } => options.is_elementwise(),
_ => false,
};
if independent {
ae.inputs_rev(stack);
}
independent
})
}

/// Checks if the top-level expression node is row-separable. If this is the case, then `stack` will
/// be extended further with any nested expression nodes.
pub fn is_row_separable(stack: &mut UnitVec<Node>, ae: &AExpr, expr_arena: &Arena<AExpr>) -> bool {
Expand Down
22 changes: 22 additions & 0 deletions crates/polars-plan/src/plans/conversion/dsl_to_ir/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use either::Either;
use polars_core::chunked_array::cast::CastOptions;
use polars_core::error::feature_gated;
use polars_core::utils::{get_numeric_upcast_supertype_lossless, try_get_supertype};
use polars_ops::prelude::JoinValidation;
use polars_utils::format_pl_smallstr;
use polars_utils::itertools::Itertools;

Expand Down Expand Up @@ -156,6 +157,27 @@ pub fn resolve_join(
let schema_left = ctxt.lp_arena.get(input_left).schema(ctxt.lp_arena);
let schema_right = ctxt.lp_arena.get(input_right).schema(ctxt.lp_arena);

// Inner-joining on the same non-null constant on both sides pairs every row with every
// row: a cross join (unless the key multiplicity is to be validated).
let same_constant_key = |l: &ExprIR, r: &ExprIR| match (
ctxt.expr_arena.get(l.node()),
ctxt.expr_arena.get(r.node()),
) {
(AExpr::Literal(l), AExpr::Literal(r)) => l.is_scalar() && !l.is_null() && l == r,
_ => false,
};
if options.args.how == JoinType::Inner
&& options.args.validation == JoinValidation::ManyToMany
&& left_on
.iter()
.zip(&right_on)
.all(|(l, r)| same_constant_key(l, r))
{
options.args.how = JoinType::Cross;
left_on.clear();
right_on.clear();
}

// # Resolve scalars
//
// Scalars need to be expanded. We translate them to temporary columns added with
Expand Down
1 change: 1 addition & 0 deletions crates/polars-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository = { workspace = true }
description = "SQL transpiler for Polars. Converts SQL to Polars logical plans"

[dependencies]
polars-compute = { workspace = true, features = ["dtype-decimal"] }
polars-core = { workspace = true, features = ["rows"] }
polars-error = { workspace = true }
polars-lazy = { workspace = true, features = ["abs", "binary_encoding", "concat_str", "cov", "cross_join", "cum_agg", "dtype-array", "dtype-date", "dtype-decimal", "dtype-struct", "is_in", "list_eval", "log", "meta", "offset_by", "range", "regex", "round_series", "sign", "string_normalize", "string_pad", "string_reverse", "strings", "timezones", "trigonometry"] }
Expand Down
Loading
Loading