diff --git a/pkg-py/src/commons/_definitions/_emit_sql.py b/pkg-py/src/commons/_definitions/_emit_sql.py index e58ffc58..ab6e6c8c 100644 --- a/pkg-py/src/commons/_definitions/_emit_sql.py +++ b/pkg-py/src/commons/_definitions/_emit_sql.py @@ -43,6 +43,8 @@ "weeks": "604800000000", } +_NULL_DATETIME = {"snowflake": "TIMESTAMP_TZ", "databricks": "TIMESTAMP"} + _SIMPLE_FUNCTIONS = { "length": "length", "lower": "lower", @@ -424,11 +426,15 @@ def _temporal_shift(node: Ir, state: _State, selected: str | None) -> str | None interval becomes a count of microseconds. """ attrs = node.attrs + dialect = state.dialect + # The checker accepts an untyped NULL as the interval, which shifts to + # NULL. Neither warehouse adds a bare NULL to a date, so it is typed. + if node.type == "datetime" and "null" in (attrs["lhs"].kind, attrs["rhs"].kind): + return f"CAST(NULL AS {_NULL_DATETIME[dialect]})" lhs_interval = attrs["lhs"].type == "interval" rhs_interval = attrs["rhs"].type == "interval" if not lhs_interval and not rhs_interval: return None - dialect = state.dialect if lhs_interval and attrs["op"] == "-": _unsupported( dialect, diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 094b8d93..e5f5213f 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,5 +1,7 @@ # commons (development version) +* A definition that adds `NULL` to a date, such as `created + NULL`, now compiles to SQL that Snowflake accepts. It compiles to a typed `NULL` timestamp (#441). + * A definition that divides by a compound expression, such as `amount / (ratio * 2)`, now compiles to the correct Snowflake and Databricks SQL. The parentheses around the divisor were dropped, so the query divided by `ratio` alone (#439). * `commons()` gains a `mode` argument. With `mode = "trusted only"`, the agent answers only with trusted calculations. It never writes its own SQL or R, and it tells the user when no trusted calculation answers a question. These answers carry no provenance markers. diff --git a/pkg-r/R/definition-emit-sql.R b/pkg-r/R/definition-emit-sql.R index 127bf663..a5220b2e 100644 --- a/pkg-r/R/definition-emit-sql.R +++ b/pkg-r/R/definition-emit-sql.R @@ -582,6 +582,17 @@ definition_sql_similar <- function(node, state, dialect, selected) { } definition_sql_temporal_shift <- function(node, state, dialect, selected) { + # The checker accepts an untyped NULL as the interval, which shifts to + # NULL. Neither warehouse adds a bare NULL to a date, so it is typed. + if ( + identical(node$type, "datetime") && + "null" %in% c(node$lhs$kind, node$rhs$kind) + ) { + return(sprintf( + "CAST(NULL AS %s)", + definition_sql_null_datetime[[dialect]] + )) + } lhs_interval <- identical(node$lhs$type, "interval") rhs_interval <- identical(node$rhs$type, "interval") if (!lhs_interval && !rhs_interval) { @@ -775,6 +786,11 @@ definition_sql_abort_unsupported <- function(dialect, what, why) { ) } +definition_sql_null_datetime <- list( + snowflake = "TIMESTAMP_TZ", + databricks = "TIMESTAMP" +) + definition_sql_nan_note <- list( snowflake = paste( "Snowflake orders a NaN as equal to itself and greater than every number,", diff --git a/pkg-r/tests/testthat/fixtures/shared/definition-warehouse-sql.json b/pkg-r/tests/testthat/fixtures/shared/definition-warehouse-sql.json index a5489d87..52a329eb 100644 --- a/pkg-r/tests/testthat/fixtures/shared/definition-warehouse-sql.json +++ b/pkg-r/tests/testthat/fixtures/shared/definition-warehouse-sql.json @@ -73,6 +73,10 @@ "survey::dynamic match": { "SQL(snowflake)": { "notes_contain": ["newline characters"] } }, + "survey::nullable date shift": { + "SQL(snowflake)": { "code": "CAST(NULL AS TIMESTAMP_TZ)" }, + "SQL(databricks)": { "code": "CAST(NULL AS TIMESTAMP)" } + }, "survey::negative quotient": { "SQL(snowflake)": { "code_contains": [ diff --git a/tests/shared/definition-warehouse-sql.json b/tests/shared/definition-warehouse-sql.json index a5489d87..52a329eb 100644 --- a/tests/shared/definition-warehouse-sql.json +++ b/tests/shared/definition-warehouse-sql.json @@ -73,6 +73,10 @@ "survey::dynamic match": { "SQL(snowflake)": { "notes_contain": ["newline characters"] } }, + "survey::nullable date shift": { + "SQL(snowflake)": { "code": "CAST(NULL AS TIMESTAMP_TZ)" }, + "SQL(databricks)": { "code": "CAST(NULL AS TIMESTAMP)" } + }, "survey::negative quotient": { "SQL(snowflake)": { "code_contains": [