Skip to content

Commit 8e9d38c

Browse files
authored
fix(cubesql): Support EXTRACT(EPOCH) over timestamp diff on Snowflake (cube-js#11105)
1 parent c79eb98 commit 8e9d38c

4 files changed

Lines changed: 118 additions & 2 deletions

File tree

packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ export class SnowflakeQuery extends BaseQuery {
114114
templates.functions.BTRIM = 'TRIM({{ args_concat }})';
115115
templates.functions.STRING_AGG = 'LISTAGG({% if distinct %}DISTINCT {% endif %}{{ args_concat }})';
116116
templates.expressions.extract = 'EXTRACT({{ date_part }} FROM {{ expr }})';
117+
// Snowflake can't EXTRACT(EPOCH FROM <interval>), so the epoch of a timestamp
118+
// difference (left - right) is rendered as fractional seconds between them.
119+
// TIMESTAMPDIFF is measured once at microsecond granularity (no per-second
120+
// boundary rounding) and divided to seconds, matching Postgres' fractional
121+
// EXTRACT(EPOCH FROM interval).
122+
templates.expressions.extract_epoch_diff = 'TIMESTAMPDIFF(MICROSECOND, {{ right }}, {{ left }}) / 1000000';
117123
templates.expressions.interval = 'INTERVAL \'{{ interval }}\'';
118124
templates.expressions.timestamp_literal = '\'{{ value }}\'::timestamp_tz';
119125
templates.expressions.like = '{{ expr }} {% if negated %}NOT {% endif %}LIKE {{ pattern }}{% if default_escape %} ESCAPE \'\\\\\'{% endif %}';

rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3063,6 +3063,22 @@ impl WrappedSelectNode {
30633063
))
30643064
}
30653065

3066+
/// Returns the operands of a timestamp/date subtraction `left - right`,
3067+
/// peeling any cast wrapping the subtraction. Used to rewrite
3068+
/// `EXTRACT(EPOCH FROM (left - right))` for dialects that don't support
3069+
/// taking the epoch of an interval.
3070+
fn timestamp_diff_operands(expr: &Expr) -> Option<(&Expr, &Expr)> {
3071+
match expr {
3072+
Expr::BinaryExpr {
3073+
left,
3074+
op: Operator::Minus,
3075+
right,
3076+
} => Some((left, right)),
3077+
Expr::Cast { expr, .. } => Self::timestamp_diff_operands(expr),
3078+
_ => None,
3079+
}
3080+
}
3081+
30663082
#[inline(never)]
30673083
fn generate_sql_for_scalar_function<'ctx>(
30683084
mut sql_query: SqlQuery,
@@ -3124,6 +3140,42 @@ impl WrappedSelectNode {
31243140
date_part
31253141
)));
31263142
}
3143+
// Some dialects (e.g. Snowflake) can't EXTRACT(EPOCH FROM <interval>),
3144+
// i.e. take the epoch of a timestamp difference `a - b`. When the
3145+
// dialect provides a dedicated template, render the difference as a
3146+
// diff in seconds instead.
3147+
let sql_templates = sql_generator.get_sql_templates();
3148+
if date_part.eq_ignore_ascii_case("epoch")
3149+
&& sql_templates.contains_template("expressions/extract_epoch_diff")
3150+
{
3151+
if let Some((left, right)) = Self::timestamp_diff_operands(&args[1]) {
3152+
let (left_sql, query) = Self::generate_sql_for_expr(
3153+
sql_query,
3154+
sql_generator.clone(),
3155+
left.clone(),
3156+
push_to_cube_context,
3157+
subqueries,
3158+
)?;
3159+
let (right_sql, query) = Self::generate_sql_for_expr(
3160+
query,
3161+
sql_generator.clone(),
3162+
right.clone(),
3163+
push_to_cube_context,
3164+
subqueries,
3165+
)?;
3166+
return Ok((
3167+
sql_templates
3168+
.extract_epoch_diff_expr(left_sql, right_sql)
3169+
.map_err(|e| {
3170+
DataFusionError::Internal(format!(
3171+
"Can't generate SQL for scalar function: {}",
3172+
e
3173+
))
3174+
})?,
3175+
query,
3176+
));
3177+
}
3178+
}
31273179
let (arg_sql, query) = Self::generate_sql_for_expr(
31283180
sql_query,
31293181
sql_generator.clone(),
@@ -3132,8 +3184,7 @@ impl WrappedSelectNode {
31323184
subqueries,
31333185
)?;
31343186
return Ok((
3135-
sql_generator
3136-
.get_sql_templates()
3187+
sql_templates
31373188
.extract_expr(date_part.to_string(), arg_sql)
31383189
.map_err(|e| {
31393190
DataFusionError::Internal(format!(

rust/cubesql/cubesql/src/compile/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16964,6 +16964,51 @@ LIMIT {{ limit }}{% endif %}"#.to_string(),
1696416964
assert!(sql.contains("unix_timestamp"));
1696516965
}
1696616966

16967+
#[tokio::test]
16968+
async fn test_extract_epoch_diff_pushdown() {
16969+
if !Rewriter::sql_push_down_enabled() {
16970+
return;
16971+
}
16972+
init_testing_logger();
16973+
16974+
// EXTRACT(EPOCH FROM (a - b)) — epoch of a timestamp difference.
16975+
let query = "
16976+
SELECT customer_gender,
16977+
AVG(EXTRACT(EPOCH FROM (order_date - last_mod)) / 86400) AS avg_days
16978+
FROM KibanaSampleDataEcommerce
16979+
GROUP BY 1
16980+
";
16981+
16982+
// Generic (no dedicated template) keeps EXTRACT(EPOCH FROM (a - b)).
16983+
let query_plan =
16984+
convert_select_to_query_plan(query.to_string(), DatabaseProtocol::PostgreSQL).await;
16985+
let logical_plan = query_plan.as_logical_plan();
16986+
let sql = logical_plan.find_cube_scan_wrapped_sql().wrapped_sql.sql;
16987+
assert!(sql.contains("EXTRACT(epoch"));
16988+
16989+
// Snowflake-style: epoch of a difference is rendered as a seconds diff.
16990+
let query_plan = convert_select_to_query_plan_customized(
16991+
query.to_string(),
16992+
DatabaseProtocol::PostgreSQL,
16993+
vec![
16994+
(
16995+
"expressions/extract".to_string(),
16996+
"EXTRACT({{ date_part }} FROM {{ expr }})".to_string(),
16997+
),
16998+
(
16999+
"expressions/extract_epoch_diff".to_string(),
17000+
"TIMESTAMPDIFF(MICROSECOND, {{ right }}, {{ left }}) / 1000000".to_string(),
17001+
),
17002+
],
17003+
)
17004+
.await;
17005+
17006+
let logical_plan = query_plan.as_logical_plan();
17007+
let sql = logical_plan.find_cube_scan_wrapped_sql().wrapped_sql.sql;
17008+
assert!(!sql.to_uppercase().contains("EXTRACT(EPOCH"));
17009+
assert!(sql.contains("TIMESTAMPDIFF(MICROSECOND,"));
17010+
}
17011+
1696717012
#[tokio::test]
1696817013
async fn test_push_down_to_grouped_query_with_filters() {
1696917014
if !Rewriter::sql_push_down_enabled() {

rust/cubesql/cubesql/src/transport/service.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,20 @@ impl SqlTemplates {
777777
)
778778
}
779779

780+
/// Renders the epoch (in seconds) of a timestamp difference `left - right`.
781+
/// Used for dialects (e.g. Snowflake) where `EXTRACT(EPOCH FROM (left - right))`
782+
/// is invalid because EPOCH can't be extracted from an interval.
783+
pub fn extract_epoch_diff_expr(
784+
&self,
785+
left: String,
786+
right: String,
787+
) -> Result<String, CubeError> {
788+
self.render_template(
789+
"expressions/extract_epoch_diff",
790+
context! { left => left, right => right },
791+
)
792+
}
793+
780794
pub fn interval_any_expr(
781795
&self,
782796
interval: String,

0 commit comments

Comments
 (0)