Skip to content
This repository was archived by the owner on May 7, 2026. It is now read-only.
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
20 changes: 20 additions & 0 deletions ibis-server/tests/routers/v3/connector/postgres/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,3 +1279,23 @@ async def test_case_sensitive_without_quote(client, connection_info):
"O_orderkey": "int32",
"O_custkey": "int32",
}


async def test_to_char_numeric(client, manifest_str, connection_info):
response = await client.post(
url=f"{base_url}/query",
json={
"connectionInfo": connection_info,
"manifestStr": manifest_str,
"sql": "SELECT to_char(1234, '9999') AS formatted_number, to_char(1234.567, '0000.00') AS formatted_double",
},
headers={X_WREN_FALLBACK_DISABLE: "true"},
)
assert response.status_code == 200
result = response.json()
assert result["data"][0][0] == " 1234"
assert result["data"][0][1] == " 1234.57"
assert result["dtypes"] == {
"formatted_number": "string",
"formatted_double": "string",
}
Comment thread
goldmedal marked this conversation as resolved.
21 changes: 21 additions & 0 deletions wren-core/core/src/mdl/function/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Creates a singleton `ScalarUDF` of the `$UDF` function and a function
/// named `$NAME` which returns that singleton.
///
/// This is used to ensure creating the list of `ScalarUDF` only happens once.
#[macro_export]
macro_rules! make_udf_function {
($UDF:ty, $NAME:ident) => {
#[doc = concat!("Return a [`ScalarUDF`](datafusion_expr::ScalarUDF) implementation of ", stringify!($NAME))]
pub fn $NAME() -> std::sync::Arc<datafusion::logical_expr::ScalarUDF> {
// Singleton instance of the function
static INSTANCE: std::sync::LazyLock<
std::sync::Arc<datafusion::logical_expr::ScalarUDF>,
> = std::sync::LazyLock::new(|| {
std::sync::Arc::new(datafusion::logical_expr::ScalarUDF::new_from_impl(
<$UDF>::new(),
))
});
std::sync::Arc::clone(&INSTANCE)
}
Comment thread
goldmedal marked this conversation as resolved.
};
}
1 change: 1 addition & 0 deletions wren-core/core/src/mdl/function/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod aggregate;
mod macros;
mod remote_function;
mod scalar;
mod table;
Expand Down
6 changes: 6 additions & 0 deletions wren-core/core/src/mdl/function/scalar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ use datafusion::{
logical_expr::ScalarUDF,
};

use crate::make_udf_function;

mod to_char;

make_udf_function!(to_char::ToCharFunc, to_char);

pub fn scalar_functions() -> Vec<Arc<ScalarUDF>> {
vec![
// datefusion core
Expand Down
83 changes: 83 additions & 0 deletions wren-core/core/src/mdl/function/scalar/to_char.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::any::Any;

use datafusion::arrow::datatypes::DataType;
use datafusion::common::not_impl_err;
use datafusion::common::types::{
logical_date, logical_float32, logical_float64, logical_string,
};
use datafusion::error::Result;
use datafusion::logical_expr::TypeSignatureClass;
use datafusion::logical_expr::{
Coercion, ColumnarValue, DocSection, Documentation, ScalarFunctionArgs,
ScalarUDFImpl, Signature, TypeSignature, Volatility,
};

#[derive(Debug)]
pub struct ToCharFunc {
signature: Signature,
aliases: Vec<String>,
doc: Option<Documentation>,
}

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

impl ToCharFunc {
pub fn new() -> Self {
Self {
signature: Signature::one_of(vec![
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Duration), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Interval), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Native(logical_date())), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Time), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Timestamp), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Integer), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Native(logical_float64())), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Coercible(vec![Coercion::new_exact(TypeSignatureClass::Native(logical_float32())), Coercion::new_exact(TypeSignatureClass::Native(logical_string()))]),
TypeSignature::Exact(vec![DataType::Decimal128(38, 10), DataType::Utf8]),
TypeSignature::Exact(vec![DataType::Decimal256(38, 10), DataType::Utf8]),
Comment thread
goldmedal marked this conversation as resolved.
], Volatility::Immutable,),
aliases: vec!["date_format".to_string()],
doc: Some(Documentation::builder(
DocSection { include: false, label: "Time and Date Functions", description: None },
"Returns a string representation of a date, time, timestamp or duration based on a [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).",
"to_char(expression, format)")
.with_argument("expression", "The date, time, timestamp or duration to format.")
.with_argument("format", "The [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) string to use for formatting.")
.build()),
}
}
}

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

fn name(&self) -> &str {
"to_char"
}

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

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

fn invoke_with_args(&self, _args: ScalarFunctionArgs) -> Result<ColumnarValue> {
not_impl_err!("to_char is not implemented")
}

fn aliases(&self) -> &[String] {
&self.aliases
}

fn documentation(&self) -> Option<&Documentation> {
self.doc.as_ref()
}
}
42 changes: 42 additions & 0 deletions wren-core/core/src/mdl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3732,6 +3732,48 @@ mod test {
Ok(())
}

#[tokio::test]
async fn test_to_char() -> Result<()> {
let ctx = create_wren_ctx(None);
let manifest = ManifestBuilder::new()
.catalog("wren")
.schema("test")
.model(
ModelBuilder::new("customer")
.table_reference("customer")
.column(ColumnBuilder::new("c_date", "date").build())
.column(ColumnBuilder::new("c_timestamp", "timestamp").build())
.column(ColumnBuilder::new("c_timestamptz", "timestamptz").build())
.column(ColumnBuilder::new("c_int", "int").build())
.column(ColumnBuilder::new("c_bigint", "bigint").build())
.column(ColumnBuilder::new("c_float", "float").build())
.column(ColumnBuilder::new("c_double", "double").build())
.column(ColumnBuilder::new("c_decimal", "decimal").build())
.build(),
)
.data_source(DataSource::BigQuery)
.build();
let analyzed_mdl = Arc::new(AnalyzedWrenMDL::analyze(
manifest,
Arc::new(HashMap::default()),
Mode::Unparse,
)?);
let headers = Arc::new(HashMap::default());
let sql = "SELECT to_char(c_date, '%Y-%m-%d'), to_char(c_timestamp, '%Y-%m-%d'), to_char(c_timestamptz, '%Y-%m-%d') FROM customer";
assert_snapshot!(
transform_sql_with_ctx(&ctx, Arc::clone(&analyzed_mdl), &[], Arc::clone(&headers), sql).await?,
@"SELECT to_char(customer.c_date, '%Y-%m-%d'), to_char(customer.c_timestamp, '%Y-%m-%d'), to_char(customer.c_timestamptz, '%Y-%m-%d') FROM (SELECT customer.c_date, customer.c_timestamp, customer.c_timestamptz FROM (SELECT __source.c_date AS c_date, __source.c_timestamp AS c_timestamp, __source.c_timestamptz AS c_timestamptz FROM customer AS __source) AS customer) AS customer"
);

let sql = "SELECT to_char(c_int, '999'), to_char(c_bigint, '999'), to_char(c_float, '999.99'), to_char(c_double, '999.99'), to_char(c_decimal, '999.99') FROM customer";
assert_snapshot!(
transform_sql_with_ctx(&ctx, Arc::clone(&analyzed_mdl), &[], Arc::clone(&headers), sql).await?,
@"SELECT to_char(customer.c_int, '999'), to_char(customer.c_bigint, '999'), to_char(customer.c_float, '999.99'), to_char(customer.c_double, '999.99'), to_char(customer.c_decimal, '999.99') FROM (SELECT customer.c_bigint, customer.c_decimal, customer.c_double, customer.c_float, customer.c_int FROM (SELECT __source.c_bigint AS c_bigint, __source.c_decimal AS c_decimal, __source.c_double AS c_double, __source.c_float AS c_float, __source.c_int AS c_int FROM customer AS __source) AS customer) AS customer"
);

Ok(())
}

/// Return a RecordBatch with made up data about customer
fn customer() -> RecordBatch {
let custkey: ArrayRef = Arc::new(Int64Array::from(vec![1, 2, 3]));
Expand Down