From 4ec24a9140b84a7dd25049901dcbd39082708454 Mon Sep 17 00:00:00 2001 From: Weijie Guo Date: Sun, 21 Jan 2024 16:59:51 +0800 Subject: [PATCH] feat: Add `ignore_nulls` for `pl.concat_str` (#13877) --- .../src/tests/projection_queries.rs | 2 +- .../src/chunked_array/strings/concat.rs | 37 +++++++--- .../src/dsl/function_expr/strings.rs | 22 ++++-- .../polars-plan/src/dsl/functions/concat.rs | 10 ++- .../logical_plan/optimizer/simplify_expr.rs | 34 ++++++--- .../optimizer/simplify_functions.rs | 31 +++++--- crates/polars-sql/src/functions.rs | 4 +- docs/src/rust/user-guide/expressions/folds.rs | 2 +- py-polars/polars/functions/as_datatype.py | 8 +- py-polars/src/functions/lazy.rs | 4 +- .../functions/as_datatype/test_as_datatype.py | 28 ------- .../functions/as_datatype/test_concat_str.py | 73 +++++++++++++++++++ py-polars/tests/unit/functions/test_concat.py | 7 -- py-polars/tests/unit/sql/test_strings.py | 8 +- 14 files changed, 185 insertions(+), 85 deletions(-) create mode 100644 py-polars/tests/unit/functions/as_datatype/test_concat_str.py diff --git a/crates/polars-lazy/src/tests/projection_queries.rs b/crates/polars-lazy/src/tests/projection_queries.rs index 1e81f00f4b41..71e43ab10d3e 100644 --- a/crates/polars-lazy/src/tests/projection_queries.rs +++ b/crates/polars-lazy/src/tests/projection_queries.rs @@ -123,7 +123,7 @@ fn concat_str_regex_expansion() -> PolarsResult<()> { ]? .lazy(); let out = df - .select([concat_str([col(r"^b_a_\d$")], ";").alias("concatenated")]) + .select([concat_str([col(r"^b_a_\d$")], ";", false).alias("concatenated")]) .collect()?; let s = out.column("concatenated")?; assert_eq!(s, &Series::new("concatenated", ["a--;;", ";b--;", ";;c--"])); diff --git a/crates/polars-ops/src/chunked_array/strings/concat.rs b/crates/polars-ops/src/chunked_array/strings/concat.rs index 86f94a8a42b9..7689ab4fe883 100644 --- a/crates/polars-ops/src/chunked_array/strings/concat.rs +++ b/crates/polars-ops/src/chunked_array/strings/concat.rs @@ -55,12 +55,21 @@ enum ColumnIter { /// Horizontally concatenate all strings. /// /// Each array should have length 1 or a length equal to the maximum length. -pub fn hor_str_concat(cas: &[&StringChunked], delimiter: &str) -> PolarsResult { +pub fn hor_str_concat( + cas: &[&StringChunked], + delimiter: &str, + ignore_nulls: bool, +) -> PolarsResult { if cas.is_empty() { return Ok(StringChunked::full_null("", 0)); } if cas.len() == 1 { - return Ok(cas[0].clone()); + let ca = cas[0]; + return if !ignore_nulls || ca.null_count() == 0 { + Ok(ca.clone()) + } else { + Ok(ca.apply_generic(|val| Some(val.unwrap_or("")))) + }; } // Calculate the post-broadcast length and ensure everything is consistent. @@ -93,23 +102,31 @@ pub fn hor_str_concat(cas: &[&StringChunked], delimiter: &str) -> PolarsResult 0 { - buf.push_str(delimiter); - } - + let mut found_not_null_value = false; + for col in cols.iter_mut() { let val = match col { ColumnIter::Iter(i) => i.next().unwrap(), ColumnIter::Broadcast(s) => *s, }; + + if has_null && !ignore_nulls { + // We know that the result must be null, but we can't just break out of the loop, + // because all cols iterator has to be moved correctly. + continue; + } + if let Some(s) = val { + if found_not_null_value { + buf.push_str(delimiter); + } buf.push_str(s); + found_not_null_value = true; } else { has_null = true; } } - if has_null { + if !ignore_nulls && has_null { builder.append_null(); } else { builder.append_value(&buf) @@ -139,11 +156,11 @@ mod test { let a = StringChunked::new("a", &["foo", "bar"]); let b = StringChunked::new("b", &["spam", "ham"]); - let out = hor_str_concat(&[&a, &b], "_").unwrap(); + let out = hor_str_concat(&[&a, &b], "_", true).unwrap(); assert_eq!(Vec::from(&out), &[Some("foo_spam"), Some("bar_ham")]); let c = StringChunked::new("b", &["literal"]); - let out = hor_str_concat(&[&a, &b, &c], "_").unwrap(); + let out = hor_str_concat(&[&a, &b, &c], "_", true).unwrap(); assert_eq!( Vec::from(&out), &[Some("foo_spam_literal"), Some("bar_ham_literal")] diff --git a/crates/polars-plan/src/dsl/function_expr/strings.rs b/crates/polars-plan/src/dsl/function_expr/strings.rs index b673da76b182..64e121099d43 100644 --- a/crates/polars-plan/src/dsl/function_expr/strings.rs +++ b/crates/polars-plan/src/dsl/function_expr/strings.rs @@ -23,7 +23,10 @@ use crate::{map, map_as_slice}; #[derive(Clone, PartialEq, Debug, Eq, Hash)] pub enum StringFunction { #[cfg(feature = "concat_str")] - ConcatHorizontal(String), + ConcatHorizontal { + delimiter: String, + ignore_nulls: bool, + }, #[cfg(feature = "concat_str")] ConcatVertical { delimiter: String, @@ -124,7 +127,7 @@ impl StringFunction { use StringFunction::*; match self { #[cfg(feature = "concat_str")] - ConcatVertical { .. } | ConcatHorizontal(_) => mapper.with_dtype(DataType::String), + ConcatVertical { .. } | ConcatHorizontal { .. } => mapper.with_dtype(DataType::String), #[cfg(feature = "regex")] Contains { .. } => mapper.with_dtype(DataType::Boolean), CountMatches(_) => mapper.with_dtype(DataType::UInt32), @@ -194,7 +197,7 @@ impl Display for StringFunction { EndsWith { .. } => "ends_with", Extract(_) => "extract", #[cfg(feature = "concat_str")] - ConcatHorizontal(_) => "concat_horizontal", + ConcatHorizontal { .. } => "concat_horizontal", #[cfg(feature = "concat_str")] ConcatVertical { .. } => "concat_vertical", Explode => "explode", @@ -318,7 +321,10 @@ impl From for SpecialEq> { ignore_nulls, } => map!(strings::concat, &delimiter, ignore_nulls), #[cfg(feature = "concat_str")] - ConcatHorizontal(delimiter) => map_as_slice!(strings::concat_hor, &delimiter), + ConcatHorizontal { + delimiter, + ignore_nulls, + } => map_as_slice!(strings::concat_hor, &delimiter, ignore_nulls), #[cfg(feature = "regex")] Replace { n, literal } => map_as_slice!(strings::replace, literal, n), #[cfg(feature = "string_reverse")] @@ -696,13 +702,17 @@ pub(super) fn concat(s: &Series, delimiter: &str, ignore_nulls: bool) -> PolarsR } #[cfg(feature = "concat_str")] -pub(super) fn concat_hor(series: &[Series], delimiter: &str) -> PolarsResult { +pub(super) fn concat_hor( + series: &[Series], + delimiter: &str, + ignore_nulls: bool, +) -> PolarsResult { let str_series: Vec<_> = series .iter() .map(|s| s.cast(&DataType::String)) .collect::>()?; let cas: Vec<_> = str_series.iter().map(|s| s.str().unwrap()).collect(); - Ok(polars_ops::chunked_array::hor_str_concat(&cas, delimiter)?.into_series()) + Ok(polars_ops::chunked_array::hor_str_concat(&cas, delimiter, ignore_nulls)?.into_series()) } impl From for FunctionExpr { diff --git a/crates/polars-plan/src/dsl/functions/concat.rs b/crates/polars-plan/src/dsl/functions/concat.rs index 1f8e91cf5cb2..c792eba4368d 100644 --- a/crates/polars-plan/src/dsl/functions/concat.rs +++ b/crates/polars-plan/src/dsl/functions/concat.rs @@ -2,13 +2,17 @@ use super::*; #[cfg(all(feature = "concat_str", feature = "strings"))] /// Horizontally concat string columns in linear time -pub fn concat_str>(s: E, separator: &str) -> Expr { +pub fn concat_str>(s: E, separator: &str, ignore_nulls: bool) -> Expr { let input = s.as_ref().to_vec(); let separator = separator.to_string(); Expr::Function { input, - function: StringFunction::ConcatHorizontal(separator).into(), + function: StringFunction::ConcatHorizontal { + delimiter: separator, + ignore_nulls, + } + .into(), options: FunctionOptions { collect_groups: ApplyOptions::ElementWise, input_wildcard_expansion: true, @@ -45,7 +49,7 @@ pub fn format_str>(format: &str, args: E) -> PolarsResult } } - Ok(concat_str(exprs, "")) + Ok(concat_str(exprs, "", false)) } /// Concat lists entries. diff --git a/crates/polars-plan/src/logical_plan/optimizer/simplify_expr.rs b/crates/polars-plan/src/logical_plan/optimizer/simplify_expr.rs index 28a37b6ac4ed..5d7231c7cbe8 100644 --- a/crates/polars-plan/src/logical_plan/optimizer/simplify_expr.rs +++ b/crates/polars-plan/src/logical_plan/optimizer/simplify_expr.rs @@ -284,17 +284,23 @@ fn string_addition_to_linear_concat( AExpr::Function { input: input_left, function: - ref - fun_l @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep_l)), + ref fun_l @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep_l, + ignore_nulls: ignore_nulls_l, + }), options, }, AExpr::Function { input: input_right, - function: FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep_r)), + function: + FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep_r, + ignore_nulls: ignore_nulls_r, + }), .. }, ) => { - if sep_l.is_empty() && sep_r.is_empty() { + if sep_l.is_empty() && sep_r.is_empty() && ignore_nulls_l == ignore_nulls_r { let mut input = Vec::with_capacity(input_left.len() + input_right.len()); input.extend_from_slice(input_left); input.extend_from_slice(input_right); @@ -312,12 +318,15 @@ fn string_addition_to_linear_concat( AExpr::Function { input, function: - ref fun @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep)), + ref fun @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep, + ignore_nulls, + }), options, }, _, ) => { - if sep.is_empty() { + if sep.is_empty() && !ignore_nulls { let mut input = input.clone(); input.push(right_ae); Some(AExpr::Function { @@ -335,11 +344,14 @@ fn string_addition_to_linear_concat( AExpr::Function { input: input_right, function: - ref fun @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep)), + ref fun @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep, + ignore_nulls, + }), options, }, ) => { - if sep.is_empty() { + if sep.is_empty() && !ignore_nulls { let mut input = Vec::with_capacity(1 + input_right.len()); input.push(left_ae); input.extend_from_slice(input_right); @@ -354,7 +366,11 @@ fn string_addition_to_linear_concat( }, _ => Some(AExpr::Function { input: vec![left_ae, right_ae], - function: StringFunction::ConcatHorizontal("".to_string()).into(), + function: StringFunction::ConcatHorizontal { + delimiter: "".to_string(), + ignore_nulls: false, + } + .into(), options: FunctionOptions { collect_groups: ApplyOptions::ElementWise, input_wildcard_expansion: true, diff --git a/crates/polars-plan/src/logical_plan/optimizer/simplify_functions.rs b/crates/polars-plan/src/logical_plan/optimizer/simplify_functions.rs index 97d0a57a2a4f..5ec3655effa1 100644 --- a/crates/polars-plan/src/logical_plan/optimizer/simplify_functions.rs +++ b/crates/polars-plan/src/logical_plan/optimizer/simplify_functions.rs @@ -35,17 +35,18 @@ pub(super) fn optimize_functions( }, // flatten nested concat_str calls #[cfg(all(feature = "strings", feature = "concat_str"))] - function @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep)) - if sep.is_empty() => - { + function @ FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep, + ignore_nulls, + }) if sep.is_empty() => { if input .iter() - .any(|node| is_string_concat(expr_arena.get(*node))) + .any(|node| is_string_concat(expr_arena.get(*node), *ignore_nulls)) { let mut new_inputs = Vec::with_capacity(input.len() * 2); for node in input { - match get_string_concat_input(*node, expr_arena) { + match get_string_concat_input(*node, expr_arena, *ignore_nulls) { Some(inp) => new_inputs.extend_from_slice(inp), None => new_inputs.push(*node), } @@ -89,23 +90,31 @@ pub(super) fn optimize_functions( } #[cfg(all(feature = "strings", feature = "concat_str"))] -fn is_string_concat(ae: &AExpr) -> bool { +fn is_string_concat(ae: &AExpr, ignore_nulls: bool) -> bool { matches!(ae, AExpr::Function { function:FunctionExpr::StringExpr( - StringFunction::ConcatHorizontal(sep), + StringFunction::ConcatHorizontal{delimiter: sep, ignore_nulls: func_inore_nulls}, ), .. - } if sep.is_empty()) + } if sep.is_empty() && *func_inore_nulls == ignore_nulls) } #[cfg(all(feature = "strings", feature = "concat_str"))] -fn get_string_concat_input(node: Node, expr_arena: &Arena) -> Option<&[Node]> { +fn get_string_concat_input( + node: Node, + expr_arena: &Arena, + ignore_nulls: bool, +) -> Option<&[Node]> { match expr_arena.get(node) { AExpr::Function { input, - function: FunctionExpr::StringExpr(StringFunction::ConcatHorizontal(sep)), + function: + FunctionExpr::StringExpr(StringFunction::ConcatHorizontal { + delimiter: sep, + ignore_nulls: func_ignore_nulls, + }), .. - } if sep.is_empty() => Some(input), + } if sep.is_empty() && *func_ignore_nulls == ignore_nulls => Some(input), _ => None, } } diff --git a/crates/polars-sql/src/functions.rs b/crates/polars-sql/src/functions.rs index 86f4734f5944..db4adfa30917 100644 --- a/crates/polars-sql/src/functions.rs +++ b/crates/polars-sql/src/functions.rs @@ -839,14 +839,14 @@ impl SQLFunctionVisitor<'_> { Concat => if function.args.is_empty() { polars_bail!(InvalidOperation: "Invalid number of arguments for Concat: 0"); } else { - self.visit_variadic(|exprs: &[Expr]| concat_str(exprs, "")) + self.visit_variadic(|exprs: &[Expr]| concat_str(exprs, "", true)) }, ConcatWS => if function.args.len() < 2 { polars_bail!(InvalidOperation: "Invalid number of arguments for ConcatWS: {}", function.args.len()); } else { self.try_visit_variadic(|exprs: &[Expr]| { match &exprs[0] { - Expr::Literal(LiteralValue::String(s)) => Ok(concat_str(&exprs[1..], s)), + Expr::Literal(LiteralValue::String(s)) => Ok(concat_str(&exprs[1..], s, true)), _ => polars_bail!(InvalidOperation: "ConcatWS 'separator' must be a literal string; found {:?}", exprs[0]), } }) diff --git a/docs/src/rust/user-guide/expressions/folds.rs b/docs/src/rust/user-guide/expressions/folds.rs index 7a4a5e689321..3c16f270e443 100644 --- a/docs/src/rust/user-guide/expressions/folds.rs +++ b/docs/src/rust/user-guide/expressions/folds.rs @@ -39,7 +39,7 @@ fn main() -> Result<(), Box> { let out = df .lazy() - .select([concat_str([col("a"), col("b")], "")]) + .select([concat_str([col("a"), col("b")], "", false)]) .collect()?; println!("{:?}", out); // --8<-- [end:string] diff --git a/py-polars/polars/functions/as_datatype.py b/py-polars/polars/functions/as_datatype.py index fae7ab7868b4..02d220868075 100644 --- a/py-polars/polars/functions/as_datatype.py +++ b/py-polars/polars/functions/as_datatype.py @@ -475,6 +475,7 @@ def concat_str( exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr, separator: str = "", + ignore_nulls: bool = False, ) -> Expr: """ Horizontally concatenate columns into a single string column. @@ -492,6 +493,11 @@ def concat_str( positional arguments. separator String that will be used to separate the values of each column. + ignore_nulls + Ignore null values (default). + + If set to ``False``, null values will be propagated. + if the row contains any null values, the output is ``None``. Examples -------- @@ -524,7 +530,7 @@ def concat_str( └─────┴──────┴──────┴───────────────┘ """ exprs = parse_as_list_of_expressions(exprs, *more_exprs) - return wrap_expr(plr.concat_str(exprs, separator)) + return wrap_expr(plr.concat_str(exprs, separator, ignore_nulls)) def format(f_string: str, *args: Expr | str) -> Expr: diff --git a/py-polars/src/functions/lazy.rs b/py-polars/src/functions/lazy.rs index bedcf6739cc7..deda777bdb8f 100644 --- a/py-polars/src/functions/lazy.rs +++ b/py-polars/src/functions/lazy.rs @@ -177,9 +177,9 @@ pub fn concat_list(s: Vec) -> PyResult { } #[pyfunction] -pub fn concat_str(s: Vec, separator: &str) -> PyExpr { +pub fn concat_str(s: Vec, separator: &str, ignore_nulls: bool) -> PyExpr { let s = s.into_iter().map(|e| e.inner).collect::>(); - dsl::concat_str(s, separator).into() + dsl::concat_str(s, separator, ignore_nulls).into() } #[pyfunction] diff --git a/py-polars/tests/unit/functions/as_datatype/test_as_datatype.py b/py-polars/tests/unit/functions/as_datatype/test_as_datatype.py index d352dae78b00..c1e266933f84 100644 --- a/py-polars/tests/unit/functions/as_datatype/test_as_datatype.py +++ b/py-polars/tests/unit/functions/as_datatype/test_as_datatype.py @@ -500,34 +500,6 @@ def test_suffix_in_struct_creation() -> None: ).unnest("bar").to_dict(as_series=False) == {"a_foo": [1, 2], "c_foo": [5, 6]} -def test_concat_str() -> None: - df = pl.DataFrame({"a": ["a", "b", "c"], "b": [1, 2, 3]}) - - out = df.select([pl.concat_str(["a", "b"], separator="-")]) - assert out["a"].to_list() == ["a-1", "b-2", "c-3"] - - -def test_concat_str_wildcard_expansion() -> None: - # one function requires wildcard expansion the other need - # this tests the nested behavior - # see: #2867 - - df = pl.DataFrame({"a": ["x", "Y", "z"], "b": ["S", "o", "S"]}) - assert df.select( - pl.concat_str(pl.all()).str.to_lowercase() - ).to_series().to_list() == ["xs", "yo", "zs"] - - -def test_concat_str_with_non_utf8_col() -> None: - out = ( - pl.LazyFrame({"a": [0], "b": ["x"]}) - .select(pl.concat_str(["a", "b"], separator="-").fill_null(pl.col("a"))) - .collect() - ) - expected = pl.Series("a", ["0-x"], dtype=pl.String) - assert_series_equal(out.to_series(), expected) - - def test_format() -> None: df = pl.DataFrame({"a": ["a", "b", "c"], "b": [1, 2, 3]}) diff --git a/py-polars/tests/unit/functions/as_datatype/test_concat_str.py b/py-polars/tests/unit/functions/as_datatype/test_concat_str.py new file mode 100644 index 000000000000..85b76ffe2535 --- /dev/null +++ b/py-polars/tests/unit/functions/as_datatype/test_concat_str.py @@ -0,0 +1,73 @@ +import pytest + +import polars as pl +from polars.testing import assert_frame_equal, assert_series_equal + + +def test_concat_str_wildcard_expansion() -> None: + # one function requires wildcard expansion the other need + # this tests the nested behavior + # see: #2867 + + df = pl.DataFrame({"a": ["x", "Y", "z"], "b": ["S", "o", "S"]}) + assert df.select( + pl.concat_str(pl.all()).str.to_lowercase() + ).to_series().to_list() == ["xs", "yo", "zs"] + + +def test_concat_str_with_non_utf8_col() -> None: + out = ( + pl.LazyFrame({"a": [0], "b": ["x"]}) + .select(pl.concat_str(["a", "b"], separator="-").fill_null(pl.col("a"))) + .collect() + ) + expected = pl.Series("a", ["0-x"], dtype=pl.String) + assert_series_equal(out.to_series(), expected) + + +def test_empty_df_concat_str_11701() -> None: + df = pl.DataFrame({"a": []}) + out = df.select(pl.concat_str([pl.col("a").cast(pl.String), pl.lit("x")])) + assert_frame_equal(out, pl.DataFrame({"a": []}, schema={"a": pl.String})) + + +def test_concat_str_ignore_nulls() -> None: + df = pl.DataFrame({"a": ["a", None, "c"], "b": [None, 2, 3], "c": ["x", "y", "z"]}) + + # ignore nulls + out = df.select([pl.concat_str(["a", "b", "c"], separator="-", ignore_nulls=True)]) + assert out["a"].to_list() == ["a-x", "2-y", "c-3-z"] + # propagate nulls + out = df.select([pl.concat_str(["a", "b", "c"], separator="-", ignore_nulls=False)]) + assert out["a"].to_list() == [None, None, "c-3-z"] + + +@pytest.mark.parametrize( + "expr", + [ + "a" + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=True), + "a" + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=False), + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=True) + "a", + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=False) + "a", + pl.lit(None, dtype=pl.String) + + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=True), + pl.lit(None, dtype=pl.String) + + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=False), + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=True) + + pl.lit(None, dtype=pl.String), + pl.concat_str(pl.lit("b"), pl.lit("c"), ignore_nulls=False) + + pl.lit(None, dtype=pl.String), + pl.lit(None, dtype=pl.String) + "a", + "a" + pl.lit(None, dtype=pl.String), + pl.concat_str(None, ignore_nulls=False) + + pl.concat_str(pl.lit("b"), ignore_nulls=False), + pl.concat_str(None, ignore_nulls=True) + + pl.concat_str(pl.lit("b"), ignore_nulls=True), + ], +) +def test_simplify_str_addition_concat_str(expr: pl.Expr) -> None: + ldf = pl.LazyFrame({}).select(expr) + print(ldf.collect(simplify_expression=True)) + assert_frame_equal( + ldf.collect(simplify_expression=True), ldf.collect(simplify_expression=False) + ) diff --git a/py-polars/tests/unit/functions/test_concat.py b/py-polars/tests/unit/functions/test_concat.py index 69f400e086a3..dacd997d49f7 100644 --- a/py-polars/tests/unit/functions/test_concat.py +++ b/py-polars/tests/unit/functions/test_concat.py @@ -1,7 +1,6 @@ import pytest import polars as pl -from polars.testing import assert_frame_equal @pytest.mark.slow() @@ -21,9 +20,3 @@ def test_concat_lf_stack_overflow() -> None: for i in range(n): bar = pl.concat([bar, pl.DataFrame({"a": i}).lazy()]) assert bar.collect().shape == (1001, 1) - - -def test_empty_df_concat_str_11701() -> None: - df = pl.DataFrame({"a": []}) - out = df.select(pl.concat_str([pl.col("a").cast(pl.String), pl.lit("x")])) - assert_frame_equal(out, pl.DataFrame({"a": []}, schema={"a": pl.String})) diff --git a/py-polars/tests/unit/sql/test_strings.py b/py-polars/tests/unit/sql/test_strings.py index d1f9517c2fd6..72a74742c6c4 100644 --- a/py-polars/tests/unit/sql/test_strings.py +++ b/py-polars/tests/unit/sql/test_strings.py @@ -63,10 +63,10 @@ def test_string_concat() -> None: assert res.to_dict(as_series=False) == { "c0": ["aad", None, "ccf"], "c1": ["ad1", None, "cf3"], - "c2": ["a-d", None, "c-f"], - "c3": ["aad", None, "ccf"], - "c4": ["ad2", None, "cf6"], - "c5": ["a:d:1", None, "c:f:3"], + "c2": ["a-d", "e", "c-f"], + "c3": ["aad", "e", "ccf"], + "c4": ["ad2", "e4", "cf6"], + "c5": ["a:d:1", "e:2", "c:f:3"], "c6": ["d1!", "e2!", "f3!"], }