Skip to content

Commit 003f0d9

Browse files
robreevesclaudeCopilotCopilot
authored
[AURON #1891] Implement randn() function (#1938)
<!-- - Start the PR title with the related issue ID, e.g. '[AURON #XXXX] Short summary...'. --> # Which issue does this PR close? Closes #1891 # Rationale for this change This improves function coverage in Auron by creating a native randn implementation. # What changes are included in this PR? Adds a native randn implementation. # Are there any user-facing changes? Yes, it adds the randn function. # How was this patch tested? Added unit tests and manually tested in spark-shell. ```scala import org.apache.spark.sql.functions.randn val df = spark.range(5) val outputPath = "/tmp/spark_range_output.parquet" df.write.mode("overwrite").parquet(outputPath) val readDf = spark.read.parquet(outputPath) val resultDf = readDf.withColumn("random_normal", randn(18)) resultDf.collect ``` Output: ``` 26/01/30 15:41:22 WARN NativeHelper: memory total: 1408.0 MiB, onheap: 1024.0 MiB, offheap: 384.0 MiB 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan 26/01/30 15:41:24 WARN AuronCallNativeWrapper: Start executing native plan ------ initializing auron native environment ------ initializing logging with level: info 2026-01-30 15:41:24.368 (+0.000s) [INFO] [auron::exec:73] (stage: 0, partition: 0, tid: 0) - initializing JNI bridge 2026-01-30 15:41:24.369 (+0.001s) [INFO] [auron_jni_bridge::jni_bridge:473] (stage: 0, partition: 0, tid: 0) - Initializing JavaClasses... 2026-01-30 15:41:24.375 (+0.007s) [INFO] [auron_jni_bridge::jni_bridge:529] (stage: 0, partition: 0, tid: 0) - Initializing JavaClasses finished 2026-01-30 15:41:24.375 (+0.007s) [INFO] [auron::exec:77] (stage: 0, partition: 0, tid: 0) - initializing datafusion session 2026-01-30 15:41:24.375 (+0.007s) [INFO] [auron_memmgr:48] (stage: 0, partition: 0, tid: 0) - mem manager initialized with total memory: 230.4 MiB 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 1, tid: 12) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=1) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [], statistics: None }, FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwMS04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 472, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 472 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 5, tid: 16) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=5) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwMC04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 297, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 297 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 2, tid: 13) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=2) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwMy04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 472, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 472 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 4, tid: 15) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=4) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwNS04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 471, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 471 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }, FileGroup { files: [], statistics: None }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 3, tid: 14) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=3) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwOS04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 472, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 472 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.385 (+0.017s) [INFO] [auron::rt:146] (stage: 2, partition: 0, tid: 11) - start executing plan: ProjectExec [#3@0 AS #3, Randn(seed=18, partition=0) AS #5], schema=[#3:Int64;N, #5:Float64] RenameColumnsExec: ["#3"], schema=[#3:Int64;N] ParquetExec: limit=None, file_group=[FileGroup { files: [PartitionedFile { object_meta: ObjectMeta { location: Path { raw: "ZmlsZTovLy90bXAvc3BhcmtfcmFuZ2Vfb3V0cHV0LnBhcnF1ZXQvcGFydC0wMDAwNy04ZTkwNmRiYS0zZDg3LTRkZWMtYjM0NC1hYjdiZWUyODEwZWQtYzAwMC5zbmFwcHkucGFycXVldA" }, last_modified: 1970-01-01T00:00:00Z, size: 472, e_tag: None, version: None }, partition_values: [], range: Some(FileRange { start: 0, end: 472 }), statistics: None, extensions: None, metadata_size_hint: None }], statistics: Some(Statistics { num_rows: Exact(0), total_byte_size: Exact(0), column_statistics: [ColumnStatistics { null_count: Absent, max_value: Absent, min_value: Absent, sum_value: Absent, distinct_count: Absent }] }) }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }, FileGroup { files: [], statistics: None }], predicate=Some(Literal { value: Boolean(true), field: Field { name: "lit", data_type: Boolean, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} } }), schema=[id:Int64;N] 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 4, tid: 15) - executing parquet scan with adaptive batch size: 10000 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 0, tid: 11) - executing parquet scan with adaptive batch size: 10000 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 3, tid: 14) - executing parquet scan with adaptive batch size: 10000 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 1, tid: 12) - executing parquet scan with adaptive batch size: 10000 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 2, tid: 13) - executing parquet scan with adaptive batch size: 10000 2026-01-30 15:41:24.394 (+0.026s) [INFO] [datafusion_datasource_parquet::opener:421] (stage: 2, partition: 5, tid: 16) - executing parquet scan with adaptive batch size: 1 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 5, tid: 16) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 0, tid: 11) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 4, tid: 15) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 3, tid: 14) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=5) native execution finalizing 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 2, tid: 13) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:183] (stage: 2, partition: 1, tid: 12) - task finished 2026-01-30 15:41:24.488 (+0.120s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=5) native execution finalized 2026-01-30 15:41:24.511 (+0.143s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=3) native execution finalizing 2026-01-30 15:41:24.511 (+0.143s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=4) native execution finalizing 2026-01-30 15:41:24.511 (+0.143s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=0) native execution finalizing 2026-01-30 15:41:24.511 (+0.143s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=2) native execution finalizing 2026-01-30 15:41:24.511 (+0.143s) [INFO] [auron::rt:266] (stage: 0, partition: 0, tid: 0) - (partition=1) native execution finalizing 2026-01-30 15:41:24.512 (+0.144s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=0) native execution finalized 2026-01-30 15:41:24.512 (+0.144s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=4) native execution finalized 2026-01-30 15:41:24.512 (+0.144s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=1) native execution finalized 2026-01-30 15:41:24.512 (+0.144s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=2) native execution finalized 2026-01-30 15:41:24.512 (+0.144s) [INFO] [auron::rt:274] (stage: 0, partition: 0, tid: 0) - (partition=3) native execution finalized import org.apache.spark.sql.functions.randn df: org.apache.spark.sql.Dataset[Long] = [id: bigint] outputPath: String = /tmp/spark_range_output.parquet readDf: org.apache.spark.sql.DataFrame = [id: bigint] resultDf: org.apache.spark.sql.DataFrame = [id: bigint, random_normal: double] res0: Array[org.apache.spark.sql.Row] = Array([3,1.4607292672705405], [0,-0.3268302897860617], [1,-0.09087682847007866], [4,-1.2271197538792842], [2,-0.546398027932835]) ``` --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a1e48f3 commit 003f0d9

10 files changed

Lines changed: 421 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ prost = "0.14.4"
185185
prost-types = "0.14.3"
186186
prost-reflect = "0.16.4"
187187
rand = "0.9.3"
188+
rand_distr = "0.5"
188189
smallvec = "2.0.0-alpha.11"
189190
sonic-rs = "0.5.8"
190191
tempfile = "3"

native-engine/auron-planner/proto/auron.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ message PhysicalExprNode {
121121
// MonotonicIncreasingID
122122
MonotonicIncreasingIdExprNode monotonic_increasing_id_expr = 20102;
123123

124+
// SparkRandn
125+
SparkRandnExprNode spark_randn_expr = 20103;
126+
124127
// BloomFilterMightContain
125128
BloomFilterMightContainExprNode bloom_filter_might_contain_expr = 20200;
126129
}
@@ -384,6 +387,10 @@ message SparkPartitionIdExprNode {
384387
message MonotonicIncreasingIdExprNode {
385388
}
386389

390+
message SparkRandnExprNode {
391+
int64 seed = 1;
392+
}
393+
387394
message BloomFilterMightContainExprNode {
388395
string uuid = 1;
389396
PhysicalExprNode bloom_filter_expr = 2;

native-engine/auron-planner/src/planner.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ use datafusion_ext_exprs::{
5454
get_indexed_field::GetIndexedFieldExpr, get_map_value::GetMapValueExpr,
5555
named_struct::NamedStructExpr, row_num::RowNumExpr,
5656
spark_monotonically_increasing_id::SparkMonotonicallyIncreasingIdExpr,
57-
spark_partition_id::SparkPartitionIdExpr,
57+
spark_partition_id::SparkPartitionIdExpr, spark_randn::SparkRandnExpr,
5858
spark_scalar_subquery_wrapper::SparkScalarSubqueryWrapperExpr,
5959
spark_udf_wrapper::SparkUDFWrapperExpr, string_contains::StringContainsExpr,
6060
string_ends_with::StringEndsWithExpr, string_starts_with::StringStartsWithExpr,
@@ -1095,6 +1095,7 @@ impl PhysicalPlanner {
10951095
ExprType::MonotonicIncreasingIdExpr(_) => {
10961096
Arc::new(SparkMonotonicallyIncreasingIdExpr::new(self.partition_id))
10971097
}
1098+
ExprType::SparkRandnExpr(e) => Arc::new(SparkRandnExpr::new(e.seed, self.partition_id)),
10981099
ExprType::BloomFilterMightContainExpr(e) => Arc::new(BloomFilterMightContainExpr::new(
10991100
e.uuid.clone(),
11001101
self.try_parse_physical_expr_box_required(&e.bloom_filter_expr, input_schema)?,

native-engine/datafusion-ext-exprs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ jni = { workspace = true }
3535
log = { workspace = true }
3636
once_cell = { workspace = true }
3737
parking_lot = { workspace = true }
38+
rand = { workspace = true }
39+
rand_distr = { workspace = true }

native-engine/datafusion-ext-exprs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod named_struct;
2525
pub mod row_num;
2626
pub mod spark_monotonically_increasing_id;
2727
pub mod spark_partition_id;
28+
pub mod spark_randn;
2829
pub mod spark_scalar_subquery_wrapper;
2930
pub mod spark_udf_wrapper;
3031
pub mod string_contains;
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use std::{
17+
any::Any,
18+
fmt::{Debug, Display, Formatter},
19+
hash::{Hash, Hasher},
20+
sync::Arc,
21+
};
22+
23+
use arrow::{
24+
array::{Float64Array, RecordBatch},
25+
datatypes::{DataType, Schema},
26+
};
27+
use datafusion::{
28+
common::Result,
29+
logical_expr::ColumnarValue,
30+
physical_expr::{PhysicalExpr, PhysicalExprRef},
31+
};
32+
use parking_lot::Mutex;
33+
use rand::{SeedableRng, rngs::StdRng};
34+
use rand_distr::{Distribution, StandardNormal};
35+
36+
use crate::down_cast_any_ref;
37+
38+
/// Returns random values with independent and identically distributed (i.i.d.)
39+
/// samples drawn from the standard normal distribution.
40+
///
41+
/// Spark-compatible semantics:
42+
/// - RNG is seeded with `seed + partition_id`
43+
/// - RNG state advances for each row (stateful across batches)
44+
///
45+
/// Note: the underlying RNG/gaussian implementation is not intended to
46+
/// reproduce Spark's exact output sequence for a given seed/partition.
47+
pub struct SparkRandnExpr {
48+
seed: i64,
49+
partition_id: usize,
50+
rng: Mutex<StdRng>,
51+
}
52+
53+
impl SparkRandnExpr {
54+
pub fn new(seed: i64, partition_id: usize) -> Self {
55+
let effective_seed = (seed as u64).wrapping_add(partition_id as u64);
56+
Self {
57+
seed,
58+
partition_id,
59+
rng: Mutex::new(StdRng::seed_from_u64(effective_seed)),
60+
}
61+
}
62+
}
63+
64+
impl Display for SparkRandnExpr {
65+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66+
write!(
67+
f,
68+
"Randn(seed={}, partition={})",
69+
self.seed, self.partition_id
70+
)
71+
}
72+
}
73+
74+
impl Debug for SparkRandnExpr {
75+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
76+
write!(
77+
f,
78+
"Randn(seed={}, partition={})",
79+
self.seed, self.partition_id
80+
)
81+
}
82+
}
83+
84+
impl PartialEq for SparkRandnExpr {
85+
fn eq(&self, other: &Self) -> bool {
86+
self.seed == other.seed && self.partition_id == other.partition_id
87+
}
88+
}
89+
90+
impl Eq for SparkRandnExpr {}
91+
92+
impl Hash for SparkRandnExpr {
93+
fn hash<H: Hasher>(&self, state: &mut H) {
94+
self.seed.hash(state);
95+
self.partition_id.hash(state);
96+
}
97+
}
98+
99+
impl PhysicalExpr for SparkRandnExpr {
100+
fn as_any(&self) -> &dyn Any {
101+
self
102+
}
103+
104+
fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
105+
Ok(DataType::Float64)
106+
}
107+
108+
fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
109+
Ok(false)
110+
}
111+
112+
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
113+
let num_rows = batch.num_rows();
114+
let mut rng = self.rng.lock();
115+
let values =
116+
Float64Array::from_iter_values(StandardNormal.sample_iter(&mut *rng).take(num_rows));
117+
Ok(ColumnarValue::Array(Arc::new(values)))
118+
}
119+
120+
fn children(&self) -> Vec<&PhysicalExprRef> {
121+
vec![]
122+
}
123+
124+
fn with_new_children(
125+
self: Arc<Self>,
126+
_children: Vec<PhysicalExprRef>,
127+
) -> Result<PhysicalExprRef> {
128+
Ok(Arc::new(Self::new(self.seed, self.partition_id)))
129+
}
130+
131+
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
132+
write!(f, "randn({})", self.seed)
133+
}
134+
}
135+
136+
impl PartialEq<dyn Any> for SparkRandnExpr {
137+
fn eq(&self, other: &dyn Any) -> bool {
138+
down_cast_any_ref(other)
139+
.downcast_ref::<Self>()
140+
.map(|other| self.seed == other.seed && self.partition_id == other.partition_id)
141+
.unwrap_or(false)
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod tests {
147+
use std::sync::Arc;
148+
149+
use arrow::{array::RecordBatch, datatypes::Schema};
150+
use datafusion::common::{Result, cast::as_float64_array};
151+
152+
use super::*;
153+
154+
fn create_empty_batch(num_rows: usize) -> RecordBatch {
155+
let schema = Arc::new(Schema::empty());
156+
RecordBatch::try_new_with_options(
157+
schema,
158+
vec![],
159+
&arrow::array::RecordBatchOptions::new().with_row_count(Some(num_rows)),
160+
)
161+
.expect("Failed to create empty batch")
162+
}
163+
164+
#[test]
165+
fn test_randn_generates_different_values_per_row() -> Result<()> {
166+
let expr = SparkRandnExpr::new(42, 0);
167+
let batch = create_empty_batch(5);
168+
169+
let result = expr.evaluate(&batch)?;
170+
let array = result.into_array(5)?;
171+
let float_arr = as_float64_array(&array)?;
172+
173+
// Values should not be constant across rows, which verifies a value is
174+
// generated per row rather than a single value being broadcast.
175+
// (Individual samples are allowed to repeat, so we don't require all
176+
// values to be distinct.)
177+
let values: Vec<f64> = (0..5).map(|i| float_arr.value(i)).collect();
178+
assert!(
179+
values.iter().any(|&v| v != values[0]),
180+
"Expected per-row values, but all rows were identical: {values:?}"
181+
);
182+
183+
Ok(())
184+
}
185+
186+
#[test]
187+
fn test_randn_reproducible_with_same_seed() -> Result<()> {
188+
let expr1 = SparkRandnExpr::new(42, 0);
189+
let expr2 = SparkRandnExpr::new(42, 0);
190+
let batch = create_empty_batch(5);
191+
192+
let result1 = expr1.evaluate(&batch)?;
193+
let result2 = expr2.evaluate(&batch)?;
194+
195+
let arr1_binding = result1.into_array(5)?;
196+
let arr2_binding = result2.into_array(5)?;
197+
let arr1 = as_float64_array(&arr1_binding)?;
198+
let arr2 = as_float64_array(&arr2_binding)?;
199+
200+
for i in 0..5 {
201+
assert_eq!(
202+
arr1.value(i),
203+
arr2.value(i),
204+
"Same seed should produce same values"
205+
);
206+
}
207+
208+
Ok(())
209+
}
210+
211+
#[test]
212+
fn test_randn_different_seeds_produce_different_values() -> Result<()> {
213+
let expr1 = SparkRandnExpr::new(42, 0);
214+
let expr2 = SparkRandnExpr::new(123, 0);
215+
let batch = create_empty_batch(5);
216+
217+
let result1 = expr1.evaluate(&batch)?;
218+
let result2 = expr2.evaluate(&batch)?;
219+
220+
let arr1_binding = result1.into_array(5)?;
221+
let arr2_binding = result2.into_array(5)?;
222+
let arr1 = as_float64_array(&arr1_binding)?;
223+
let arr2 = as_float64_array(&arr2_binding)?;
224+
225+
// At least one value should be different
226+
let any_different = (0..5).any(|i| arr1.value(i) != arr2.value(i));
227+
assert!(
228+
any_different,
229+
"Different seeds should produce different values"
230+
);
231+
232+
Ok(())
233+
}
234+
235+
#[test]
236+
fn test_randn_different_partitions_produce_different_values() -> Result<()> {
237+
let expr1 = SparkRandnExpr::new(42, 0);
238+
let expr2 = SparkRandnExpr::new(42, 1);
239+
let batch = create_empty_batch(5);
240+
241+
let result1 = expr1.evaluate(&batch)?;
242+
let result2 = expr2.evaluate(&batch)?;
243+
244+
let arr1_binding = result1.into_array(5)?;
245+
let arr2_binding = result2.into_array(5)?;
246+
let arr1 = as_float64_array(&arr1_binding)?;
247+
let arr2 = as_float64_array(&arr2_binding)?;
248+
249+
// At least one value should be different
250+
let any_different = (0..5).any(|i| arr1.value(i) != arr2.value(i));
251+
assert!(
252+
any_different,
253+
"Different partitions should produce different values"
254+
);
255+
256+
Ok(())
257+
}
258+
259+
#[test]
260+
fn test_randn_stateful_across_batches() -> Result<()> {
261+
let expr = SparkRandnExpr::new(42, 0);
262+
let batch1 = create_empty_batch(3);
263+
let batch2 = create_empty_batch(3);
264+
265+
// Evaluate two batches sequentially
266+
let result1 = expr.evaluate(&batch1)?;
267+
let result2 = expr.evaluate(&batch2)?;
268+
269+
let arr1_binding = result1.into_array(3)?;
270+
let arr2_binding = result2.into_array(3)?;
271+
let arr1 = as_float64_array(&arr1_binding)?;
272+
let arr2 = as_float64_array(&arr2_binding)?;
273+
274+
// Collect all values
275+
let values1: Vec<f64> = (0..3).map(|i| arr1.value(i)).collect();
276+
let values2: Vec<f64> = (0..3).map(|i| arr2.value(i)).collect();
277+
278+
// Second batch should continue from where first left off (not restart)
279+
// So values should be different between batches
280+
assert_ne!(values1, values2, "Batches should have different values");
281+
282+
// Compare with fresh expr that evaluates both batches together
283+
let expr_fresh = SparkRandnExpr::new(42, 0);
284+
let batch_combined = create_empty_batch(6);
285+
let result_combined = expr_fresh.evaluate(&batch_combined)?;
286+
let arr_combined_binding = result_combined.into_array(6)?;
287+
let arr_combined = as_float64_array(&arr_combined_binding)?;
288+
289+
// First 3 values should match values1, next 3 should match values2
290+
for i in 0..3 {
291+
assert_eq!(
292+
arr_combined.value(i),
293+
values1[i],
294+
"First batch values should match"
295+
);
296+
assert_eq!(
297+
arr_combined.value(i + 3),
298+
values2[i],
299+
"Second batch values should match continuation"
300+
);
301+
}
302+
303+
Ok(())
304+
}
305+
}

0 commit comments

Comments
 (0)