A notebook or job that worked on Databricks Runtime 15.4 or 16.4 fails after moving to 17.x with errors like [CAST_INVALID_INPUT], [DIVIDE_BY_ZERO] or [ARITHMETIC_OVERFLOW]. The code didn't change. The default did. ANSI SQL mode is enabled by default in Apache Spark 4.0 and Databricks Runtime 17.0 and above. Operations that used to return NULL quietly now raise an error.
Databricks SQL warehouses already had ANSI mode on by default, so the same query can behave one way on a warehouse and another way on an older cluster.
Check which mode you're in
-- Notebook or job on a cluster / serverless
SET spark.sql.ansi.enabled;
-- SQL warehouse
SET ANSI_MODE;
In Python:
spark.conf.get("spark.sql.ansi.enabled")
Error to fix lookup
| Error class | Typical cause | Fix |
|---|---|---|
CAST_INVALID_INPUT | CAST('n/a' AS INT), or implicit cast of a non-numeric string | try_cast(col AS INT) |
CAST_OVERFLOW | Casting a value that doesn't fit, such as a large BIGINT to INT | Widen the target type or try_cast |
DIVIDE_BY_ZERO | a / b where b = 0 | try_divide(a, b) or a / NULLIF(b, 0) |
ARITHMETIC_OVERFLOW | INT + INT or * exceeding range | try_add, try_multiply, or cast to BIGINT/DECIMAL first |
NUMERIC_VALUE_OUT_OF_RANGE | Decimal result exceeds the declared precision | Increase precision, or try_cast to the decimal type |
INVALID_ARRAY_INDEX_IN_ELEMENT_AT | element_at(arr, 5) on a shorter array | try_element_at(arr, 5) |
INVALID_ARRAY_INDEX | arr[5] out of bounds | try_element_at(arr, 6) (1-based) or get(arr, 5) (0-based) |
MAP_KEY_DOES_NOT_EXIST | element_at(map, 'missing') | try_element_at(map, 'missing') |
CANNOT_PARSE_TIMESTAMP | to_timestamp on a malformed string | try_to_timestamp(col, fmt) |
CAST_OVERFLOW_IN_TABLE_INSERT | Writing a value too large for the target column | Fix the target type or cleanse the value before insert |
The exact error class names and message text vary by runtime version. The class in square brackets at the start of the message is the part to search for. The message often adds a hint, such as "Use try_cast to tolerate malformed input", which tells you the intended fix.
Find the bad rows before you change anything
Don't just wrap everything in try_ functions. First find out what the bad values are, because they may be a data quality problem upstream.
Strings that won't cast
SELECT order_amount_raw, count(*) AS rows
FROM bronze.sales.orders_raw
WHERE order_amount_raw IS NOT NULL
AND try_cast(order_amount_raw AS DECIMAL(18,2)) IS NULL
GROUP BY order_amount_raw
ORDER BY rows DESC
LIMIT 50;
Typical results include '', 'N/A', '1,234.50' and values with trailing spaces or currency symbols.
Zero divisors
SELECT count(*) AS zero_rows
FROM silver.sales.orders
WHERE quantity = 0;
Timestamps that won't parse
SELECT event_time_raw, count(*)
FROM bronze.web.events_raw
WHERE event_time_raw IS NOT NULL
AND try_to_timestamp(event_time_raw, 'yyyy-MM-dd HH:mm:ss') IS NULL
GROUP BY event_time_raw
LIMIT 50;
Fix patterns in SQL
Casting
-- Before (returned NULL silently without ANSI, fails with ANSI)
SELECT CAST(order_amount_raw AS DECIMAL(18,2)) AS order_amount FROM bronze.sales.orders_raw;
-- After: explicit about what to tolerate
SELECT try_cast(regexp_replace(trim(order_amount_raw), ',', '') AS DECIMAL(18,2)) AS order_amount
FROM bronze.sales.orders_raw;
Implicit casts in filters and joins
ANSI mode also catches comparisons between a number and a string that isn't a valid number:
-- Fails if any customer_code is not numeric
SELECT * FROM silver.crm.customers WHERE customer_id = customer_code;
-- Make the types match on purpose
SELECT * FROM silver.crm.customers WHERE customer_id = try_cast(customer_code AS BIGINT);
Division and arithmetic
SELECT
try_divide(revenue, quantity) AS unit_price, -- NULL when quantity = 0
revenue / NULLIF(quantity, 0) AS unit_price_alt,
try_multiply(CAST(units AS BIGINT), 1000) AS milli_units,
try_add(counter_a, counter_b) AS total_counter
FROM silver.sales.orders;
Arrays and maps
SELECT
try_element_at(split(path, '/'), 3) AS third_segment,
try_element_at(attributes, 'campaign') AS campaign
FROM bronze.web.events_raw;
Fix patterns in PySpark
Most try_ functions are available in pyspark.sql.functions. Where your version lacks one, use F.expr:
from pyspark.sql import functions as F
df = (spark.table("bronze.sales.orders_raw")
.withColumn("order_amount", F.expr("try_cast(order_amount_raw AS DECIMAL(18,2))"))
.withColumn("unit_price", F.try_divide("revenue", "quantity"))
.withColumn("event_ts", F.try_to_timestamp(F.col("event_time_raw"),
F.lit("yyyy-MM-dd HH:mm:ss"))))
Python UDFs aren't affected by ANSI mode in the same way. The errors come from Spark SQL expressions, including those generated by DataFrame methods like .cast("int").
Quarantine instead of silently nulling
If a bad value means bad data, keep the row and flag it, rather than turning it into NULL and moving on:
CREATE OR REPLACE TABLE silver.sales.orders_rejected AS
SELECT *, current_timestamp() AS rejected_at
FROM bronze.sales.orders_raw
WHERE order_amount_raw IS NOT NULL
AND try_cast(order_amount_raw AS DECIMAL(18,2)) IS NULL;
Load the good rows into silver and alert on the count in orders_rejected.
Turning ANSI mode off (last resort)
You can restore the old behaviour per session or per cluster:
SET spark.sql.ansi.enabled = false; -- clusters, serverless
SET ANSI_MODE = false; -- SQL warehouse session
On a classic cluster, you can also set spark.sql.ansi.enabled false in Compute → (cluster) → Advanced options → Spark config, or in the job cluster definition.
Warning
Disabling ANSI mode brings back silent
NULLs, wrapped-around integer overflow and truncated casts. Totals can be wrong without any error. Use it only as a short-term unblock for a Sev1, scoped to the failing job, with a ticket to fix the expressions.
Caution
Don't set
spark.sql.ansi.enabled falsein a shared cluster policy or as a workspace-wide default. Teams that already depend on ANSI errors to catch bad data would lose that protection without being told.
Prevent it next time
- Before a runtime upgrade, run each production job once on a 17.x job cluster against a copy of yesterday's data, then look for the error classes above.
- Search your repos for
CAST(,.cast(,/,element_atandto_timestampin bronze-to-silver code. That's where malformed input lands. - Use
try_castand friends only at the ingestion boundary, together with a rejected-rows table. In silver and gold, let ANSI errors fail loudly. - Keep clusters, serverless and SQL warehouses on the same ANSI setting, so a query behaves the same way everywhere it runs.
- Add a data quality check, such as a Lakeflow expectation or a simple count query, on the number of rows that fail to cast, and alert on change.