Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
273561c
functions: reverse of an array (SQL-5) and to_char/to_varchar of a bi…
techgopal Nov 14, 2026
829015c
plan, exec: filter takes the element index (x, i) -> ... (SQL-4)
techgopal Nov 14, 2026
9cfc671
tests: Spark goldens for sequence over dates and timestamps and schem…
techgopal Nov 14, 2026
9e07ad5
functions: sequence over dates and timestamps with year-month, day-ti…
techgopal Nov 14, 2026
1e68139
functions: schema_of_xml options (excludeAttribute, ignoreNamespace, …
techgopal Nov 14, 2026
c1267d5
merge main
techgopal Nov 14, 2026
e171755
functions: review fixes for SQL-6 (up-front sequence length, step sig…
techgopal Nov 14, 2026
54c8618
changelog: fragment for #767 (SQL-4, SQL-5, SQL-6)
techgopal Nov 14, 2026
b5a49cb
functions: CI and review fixes for #767 (a step that cancels out is a…
techgopal Nov 14, 2026
eba920b
functions: a calendar step's effective direction must reach stop; ref…
techgopal Nov 14, 2026
0e19784
functions: the sequence runaway guard applies to month steps only
techgopal Nov 15, 2026
f1e2c16
Merge remote-tracking branch 'origin/main' into p3/s7-sql456-fixes
techgopal Nov 15, 2026
3e532c0
python: the filter index-lambda test is no longer an expected failure
techgopal Nov 15, 2026
3d20398
functions: review fixes (Spark's combined step estimate, floor divisi…
techgopal Nov 15, 2026
54e21bc
functions: schema_of_xml names _LEGACY_ERROR_TEMP_2147 only for the t…
techgopal Nov 15, 2026
4c1aa6a
Merge remote-tracking branch 'origin/main' into p3/s7-sql456-fixes
techgopal Nov 15, 2026
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 changelog.d/767.compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`filter` takes the element index (`(x, i) -> ...`), `reverse` works on arrays, `sequence` works over dates and timestamps with year-month, day-time and calendar steps, `to_char` and `to_varchar` accept binary values (`hex`, `base64`, `utf-8`), and `schema_of_xml` honours `excludeAttribute`, `ignoreNamespace` and `prefersDecimal` and merges repeated elements the way Spark does (SQL-4, SQL-5, SQL-6).
4 changes: 2 additions & 2 deletions crates/krishiv-exec/src/planner/eval/hof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//!
//! For a batch of `rows` rows whose lists hold `total` elements, the
//! flattened batch has `total` rows: the lambda's element columns are the
//! lists' child values, its index column (two-variable `transform`) counts
//! lists' child values, its index column (two-variable `transform` or `filter`) counts
//! positions within each list, and every captured outer column repeats its
//! row's value per element (through the lists' offsets). A vectorized body
//! runs once over that batch — never one evaluation per element — and the
Expand Down Expand Up @@ -454,7 +454,7 @@ fn compile_unary(name: &str, f: &ResolvedFunction, input: Input<'_>) -> Result<A
let ArrowType::List(field) = array.data_type().clone() else {
return Err(internal("a higher-order call over a non-list"));
};
let index = name == "transform" && params.len() == 2;
let index = matches!(name, "transform" | "filter") && params.len() == 2;
let mut refs = Refs::default();
collect(
body,
Expand Down
80 changes: 69 additions & 11 deletions crates/krishiv-functions/src/registry/array_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

use krishiv_common::error::{
DatatypeMismatchSequenceWrongInputTypes, DatatypeMismatchUnexpectedInputType, KrishivError,
KrishivUnsupportedFeature, WrongNumArgsWithoutSuggestion,
WrongNumArgsWithoutSuggestion,
};
use krishiv_types::{ArrayType, DataType};

Expand Down Expand Up @@ -128,8 +128,49 @@ fn sequence_type(c: &CallTypes<'_>) -> Option<DataType> {
})
}

/// The result element type of `sequence(start, stop[, step])` over temporal bounds (Spark 4.2.0): `DATE` for two dates, else a timestamp
/// (`TIMESTAMP_NTZ` only when no bound is a plain `TIMESTAMP`; a `DATE` beside either takes its type). `None` unless some bound is
/// temporal and none is anything else.
fn temporal_sequence_type(c: &CallTypes<'_>) -> Option<DataType> {
if c.args().len() < 2 {
return None;
}
let is_time = |t: &DataType| {
matches!(
t,
DataType::Date | DataType::Timestamp | DataType::TimestampNtz
)
};
let bounds: Vec<Option<&DataType>> = (0..2).map(|i| c.data_type(i)).collect();
// An untyped NULL bound, or (under ANSI) a string, is cast to the other bound's type; at least one bound is a date or timestamp.
let castable = |t: &Option<&DataType>| match t {
None => true,
Some(DataType::String) => c.ansi(),
Some(t) => is_time(t),
};
if !bounds.iter().any(|t| t.is_some_and(is_time)) || !bounds.iter().all(castable) {
return None;
}
let has = |want: &DataType| bounds.contains(&Some(want));
Some(if has(&DataType::Timestamp) {
DataType::Timestamp
} else if has(&DataType::TimestampNtz) {
DataType::TimestampNtz
} else {
DataType::Date
})
}

/// `sequence`'s inputs: every argument is cast to the widest integral type.
fn sequence_inputs(c: &CallTypes<'_>) -> Vec<InputType> {
if let Some(t) = temporal_sequence_type(c) {
// The bounds are cast to the common temporal type; the step keeps its own (checked in the result rule).
let mut v = vec![InputType::Exact(t.clone()), InputType::Exact(t)];
if c.args().len() > 2 {
v.push(InputType::Any);
}
return v;
}
match sequence_type(c) {
Some(t) => vec![InputType::Exact(t); c.args().len()],
None => vec![InputType::Any; c.args().len()],
Expand All @@ -155,17 +196,34 @@ fn sequence(c: &CallTypes<'_>) -> Result<ArgType, KrishivError> {
c.any_nullable(),
));
}
let temporal = |t: Option<&DataType>| {
matches!(
t,
Some(DataType::Date | DataType::Timestamp | DataType::TimestampNtz)
)
};
if temporal(c.data_type(0)) || temporal(c.data_type(1)) {
return Err(KrishivUnsupportedFeature {
feature: "sequence over dates or timestamps".to_owned(),
if let Some(t) = temporal_sequence_type(c) {
// The step is a calendar, year-month or day-time interval; anything else (an integral, a string, an untyped NULL) is a type error.
let interval = |i: usize| {
matches!(
c.data_type(i),
Some(
DataType::CalendarInterval
| DataType::YearMonthInterval { .. }
| DataType::DayTimeInterval { .. }
)
)
};
if n == 3 && !interval(2) {
return Err(DatatypeMismatchSequenceWrongInputTypes {
sql_expr: call_sql("sequence", c),
function_name: "`sequence`".to_owned(),
start_type: "(\"TIMESTAMP\" or \"TIMESTAMP_NTZ\" or \"DATE\")".to_owned(),
step_type:
"(\"INTERVAL\" or \"INTERVAL YEAR TO MONTH\" or \"INTERVAL DAY TO SECOND\")"
.to_owned(),
other_start_type: "\"INTEGRAL\"".to_owned(),
}
.into());
}
.into());
return Ok(ArgType::new(
DataType::Array(ArrayType::new(t, false).into()),
c.any_nullable(),
));
}
Err(DatatypeMismatchSequenceWrongInputTypes {
sql_expr: call_sql("sequence", c),
Expand Down
10 changes: 9 additions & 1 deletion crates/krishiv-functions/src/registry/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::sync::Arc;

use krishiv_common::error::{
DatatypeMismatchNonFoldableInput, DatatypeMismatchUnexpectedInputType,
InvalidParameterValueBinaryFormat, KrishivError,
InvalidParameterValueBinaryFormat, InvalidParameterValueNull, KrishivError,
};
use krishiv_kernels::cast::{CastMode, CastOptions, TimeZoneRules, cast};
use krishiv_kernels::number_format::parse_format;
Expand Down Expand Up @@ -508,6 +508,14 @@ fn to_char_result(c: &CallTypes<'_>) -> Result<ArgType, KrishivError> {
// Binaries take `base64`/`hex`/`utf-8` (matched lower-cased, like
// Spark's `toLowerCase.trim`).
if matches!(c.data_type(0), Some(DataType::Binary)) {
// Spark's `ToCharacterBuilder` refuses a NULL format for a binary.
if matches!(c.constant(1), Some(Constant::Null)) {
return Err(InvalidParameterValueNull {
parameter: "`format`".to_owned(),
function_name: "`to_char`".to_owned(),
}
.into());
}
if let Some(pattern) = format.as_deref()
&& !matches!(
pattern.to_lowercase().trim_matches(|c: char| c <= ' '),
Expand Down
13 changes: 13 additions & 0 deletions crates/krishiv-functions/src/registry/tests/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,16 @@ fn argument_casts_follow_spark() {
InputType::Any
);
}

#[test]
fn to_char_of_a_binary_refuses_a_null_format() {
let args = [ty(DataType::Binary), ty(DataType::String)];
for name in ["to_char", "to_varchar"] {
let err = result(name, &args, &[None, Some(Constant::Null)]).unwrap_err();
assert_eq!(
err.error_class(),
ErrorClass::InvalidParameterValueNull,
"{name}"
);
}
}
9 changes: 8 additions & 1 deletion crates/krishiv-functions/src/registry/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,16 @@ pub(super) fn specs() -> Vec<Spec> {
&Inputs::Expects(vec![InputType::Strict(DataType::String)]),
string,
));
// `reverse` takes a string, a binary or an array (Spark's `TypeCollection(StringType, ArrayType)`): an array keeps its type.
out.extend(scalar(
&["reverse"],
&Inputs::Expects(vec![text_or_bytes.clone()]),
&Inputs::Built(InputBuilder(|c: &CallTypes<'_>| {
if matches!(c.data_type(0), Some(DataType::Array(_))) {
vec![InputType::Array]
} else {
vec![InputType::OneOf(vec![DataType::String, DataType::Binary])]
}
})),
first_type,
));
out.extend(scalar(&["quote"], &one_text, nullable_string));
Expand Down
Loading
Loading