Skip to content

monthname scalar function #1047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 crates/embucket-functions/src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod date_diff;
pub mod date_from_parts;
pub mod dayname;
pub mod last_day;
pub mod monthname;
pub mod next_day;
pub mod previous_day;
pub mod time_from_parts;
Expand Down
143 changes: 143 additions & 0 deletions crates/embucket-functions/src/datetime/monthname.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use chrono::{DateTime, Utc};
use datafusion::arrow::array::{Array, StringBuilder};
use datafusion::arrow::datatypes::{DataType, TimeUnit};
use datafusion::error::Result as DFResult;
use datafusion::logical_expr::TypeSignature::Coercible;
use datafusion::logical_expr::{Coercion, ColumnarValue, TypeSignatureClass};
use datafusion_common::types::logical_date;
use datafusion_common::{ScalarValue, exec_err};
use datafusion_expr::{ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility};
use std::any::Any;
use std::sync::Arc;

/// `MONTHNAME` SQL function
///
/// Extracts the three-letter month name from the specified date or timestamp.
///
/// Syntax: `MONTHNAME(<date_or_timestamp>)`
///
/// Arguments:
/// - `date_or_timestamp`: A date or timestamp value.
///
/// Example: `SELECT monthname('2025-05-08T23:39:20.123-07:00'::timestamp) AS value;`
///
/// Returns:
/// - Returns a string representing the three-letter month name
#[derive(Debug)]
pub struct MonthNameFunc {
signature: Signature,
}

impl Default for MonthNameFunc {
fn default() -> Self {
Self::new()
}
}

impl MonthNameFunc {
#[must_use]
pub fn new() -> Self {
Self {
signature: Signature::one_of(
vec![
Coercible(vec![Coercion::new_exact(TypeSignatureClass::Timestamp)]),
Coercible(vec![Coercion::new_exact(TypeSignatureClass::Native(
logical_date(),
))]),
],
Volatility::Immutable,
),
}
}
}

impl ScalarUDFImpl for MonthNameFunc {
fn as_any(&self) -> &dyn Any {
self
}

fn name(&self) -> &'static str {
"monthname"
}

fn signature(&self) -> &Signature {
&self.signature
}

fn return_type(&self, _arg_types: &[DataType]) -> DFResult<DataType> {
Ok(DataType::Utf8)
}

#[allow(
clippy::unwrap_used,
clippy::as_conversions,
clippy::cast_possible_truncation
)]
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> DFResult<ColumnarValue> {
let ScalarFunctionArgs { args, .. } = args;

match args[0].clone() {
ColumnarValue::Array(arr) => {
let mut res = StringBuilder::with_capacity(arr.len(), 1024);
for i in 0..arr.len() {
let v = ScalarValue::try_from_array(&arr, i)?
.cast_to(&DataType::Timestamp(TimeUnit::Nanosecond, None))?;
let ScalarValue::TimestampNanosecond(Some(ts), None) = v else {
return exec_err!(
"First argument must be a timestamp with nanosecond precision"
);
};
let naive = DateTime::<Utc>::from_timestamp_nanos(ts).naive_utc();
res.append_value(format!("{}", naive.format("%b")));
}

Ok(ColumnarValue::Array(Arc::new(res.finish())))
}
ColumnarValue::Scalar(v) => {
let v = v.cast_to(&DataType::Timestamp(TimeUnit::Nanosecond, None))?;

let ScalarValue::TimestampNanosecond(Some(ts), None) = v else {
return exec_err!(
"First argument must be a timestamp with nanosecond precision"
);
};
let naive = DateTime::<Utc>::from_timestamp_nanos(ts).naive_utc();

Ok(ColumnarValue::Scalar(ScalarValue::Utf8(Some(
naive.format("%b").to_string(),
))))
}
}
}
}

crate::macros::make_udf_function!(MonthNameFunc);
#[cfg(test)]
mod tests {
use super::*;
use datafusion::prelude::SessionContext;
use datafusion_common::assert_batches_eq;
use datafusion_expr::ScalarUDF;

#[tokio::test]
async fn test_basic() -> DFResult<()> {
let ctx = SessionContext::new();
ctx.register_udf(ScalarUDF::from(MonthNameFunc::new()));

let sql = "SELECT monthname('2025-06-08T23:39:20.123-07:00'::date) AS value;";
let result = ctx.sql(sql).await?.collect().await?;

assert_batches_eq!(
&[
"+-------+",
"| value |",
"+-------+",
"| Jun |",
"+-------+",
],
&result
);

Ok(())
}
}
1 change: 1 addition & 0 deletions crates/embucket-functions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub fn register_udfs(registry: &mut dyn FunctionRegistry) -> Result<()> {
datetime::date_from_parts::get_udf(),
datetime::last_day::get_udf(),
datetime::add_months::get_udf(),
datetime::monthname::get_udf(),
datetime::dayname::get_udf(),
datetime::previous_day::get_udf(),
datetime::next_day::get_udf(),
Expand Down
1 change: 1 addition & 0 deletions crates/embucket-functions/src/tests/datetime/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod monthname;
7 changes: 7 additions & 0 deletions crates/embucket-functions/src/tests/datetime/monthname.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::test_query;

test_query!(
basic_monthname,
"SELECT monthname('2025-06-08T23:39:20.123-07:00'::date) AS value;",
snapshot_path = "monthname"
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/embucket-functions/src/tests/datetime/monthname.rs
assertion_line: 3
description: "\"SELECT monthname('2025-06-08T23:39:20.123-07:00'::date) AS value;\""
---
Ok(
[
"+-------+",
"| value |",
"+-------+",
"| Jun |",
"+-------+",
],
)
1 change: 1 addition & 0 deletions crates/embucket-functions/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod aggregate;
mod datetime;
mod query;
mod string_binary;
mod utils;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -895,12 +895,6 @@ pub const DATETIME_FUNCTIONS: &[(&str, FunctionInfo)] = &[
)
.with_docs("https://docs.snowflake.com/en/sql-reference/functions/year")
),
("MONTHNAME", FunctionInfo::new(
"MONTHNAME",
"Extracts the three-letter month name from the specified date or timestamp."
)
.with_docs("https://docs.snowflake.com/en/sql-reference/functions/monthname")
),
("MONTHS_BETWEEN", FunctionInfo::new(
"MONTHS_BETWEEN",
"Returns the number of months between two DATE or TIMESTAMP values."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ max
md5
mean
median
min
min
monthname
named_struct
nanvl
next_day
Expand Down