diff --git a/vortex-array/src/compute/conformance/binary_numeric.rs b/vortex-array/src/compute/conformance/binary_numeric.rs
index ce3eba940df..7581496ffab 100644
--- a/vortex-array/src/compute/conformance/binary_numeric.rs
+++ b/vortex-array/src/compute/conformance/binary_numeric.rs
@@ -20,6 +20,7 @@
 //! - Subtraction (`-`)
 //! - Multiplication (`*`)
 //! - Division (`/`)
+//! - Safe division (`//`) — like division but returns 0 on a zero divisor
 
 use itertools::Itertools;
 use num_traits::Num;
@@ -104,11 +105,12 @@ where
         .cast(array.dtype())
         .vortex_expect("operation should succeed in conformance test");
 
-    let operators: [NumericOperator; 4] = [
+    let operators: [NumericOperator; 5] = [
         NumericOperator::Add,
         NumericOperator::Sub,
         NumericOperator::Mul,
         NumericOperator::Div,
+        NumericOperator::SafeDiv,
     ];
 
     for operator in operators {
@@ -338,13 +340,15 @@ where
         .cast(array.dtype())
         .vortex_expect("operation should succeed in conformance test");
 
-    // Only test operators that make sense for the given scalar
+    // Only test operators that make sense for the given scalar.
+    // Regular Div is skipped when the scalar is zero (it would error); SafeDiv is always
+    // exercised because it is specifically defined to yield zero on a zero divisor.
     let operators = if scalar_value == T::zero() {
-        // Skip division by zero
         vec![
             NumericOperator::Add,
             NumericOperator::Sub,
             NumericOperator::Mul,
+            NumericOperator::SafeDiv,
         ]
     } else {
         vec![
@@ -352,6 +356,7 @@ where
             NumericOperator::Sub,
             NumericOperator::Mul,
             NumericOperator::Div,
+            NumericOperator::SafeDiv,
         ]
     };
 
diff --git a/vortex-array/src/scalar/typed_view/decimal/scalar.rs b/vortex-array/src/scalar/typed_view/decimal/scalar.rs
index b0890177f21..96893c341c8 100644
--- a/vortex-array/src/scalar/typed_view/decimal/scalar.rs
+++ b/vortex-array/src/scalar/typed_view/decimal/scalar.rs
@@ -222,13 +222,17 @@ impl<'a> DecimalScalar<'a> {
         // Handle null cases using SQL semantics
         let result_value = match (self.decimal_value, other.decimal_value) {
             (None, _) | (_, None) => None,
+            (Some(_), Some(rhs)) if op == NumericOperator::SafeDiv && rhs.is_zero() => {
+                // Non-null zero divisor: return zero rather than erroring.
+                Some(DecimalValue::zero(&self.decimal_type))
+            }
             (Some(lhs), Some(rhs)) => {
                 // Perform the operation
                 let operation_result = match op {
                     NumericOperator::Add => lhs.checked_add(&rhs),
                     NumericOperator::Sub => lhs.checked_sub(&rhs),
                     NumericOperator::Mul => lhs.checked_mul(&rhs),
-                    NumericOperator::Div => lhs.checked_div(&rhs),
+                    NumericOperator::Div | NumericOperator::SafeDiv => lhs.checked_div(&rhs),
                 }?;
 
                 // Check if the result fits within the precision constraints
diff --git a/vortex-array/src/scalar/typed_view/decimal/tests.rs b/vortex-array/src/scalar/typed_view/decimal/tests.rs
index ca5b06778d9..39c68e07b84 100644
--- a/vortex-array/src/scalar/typed_view/decimal/tests.rs
+++ b/vortex-array/src/scalar/typed_view/decimal/tests.rs
@@ -885,6 +885,31 @@ fn test_decimal_scalar_checked_div_by_zero() {
     assert_eq!(result, None);
 }
 
+#[test]
+fn test_decimal_scalar_safe_div_by_zero() {
+    use crate::scalar::NumericOperator;
+
+    let decimal1 = Scalar::decimal(
+        DecimalValue::I64(1000),
+        DecimalDType::new(10, 2),
+        Nullability::NonNullable,
+    );
+    let scalar1 = decimal1.as_decimal();
+
+    let decimal2 = Scalar::decimal(
+        DecimalValue::I64(0),
+        DecimalDType::new(10, 2),
+        Nullability::NonNullable,
+    );
+    let scalar2 = decimal2.as_decimal();
+
+    // SafeDiv returns zero (not None) when the non-null divisor is zero.
+    let result = scalar1
+        .checked_binary_numeric(&scalar2, NumericOperator::SafeDiv)
+        .unwrap();
+    assert!(result.decimal_value().unwrap().is_zero());
+}
+
 #[test]
 fn test_decimal_scalar_null_handling() {
     use crate::scalar::NumericOperator;
diff --git a/vortex-array/src/scalar/typed_view/primitive/numeric_operator.rs b/vortex-array/src/scalar/typed_view/primitive/numeric_operator.rs
index 387d6a71cce..bffa85e7da0 100644
--- a/vortex-array/src/scalar/typed_view/primitive/numeric_operator.rs
+++ b/vortex-array/src/scalar/typed_view/primitive/numeric_operator.rs
@@ -18,6 +18,9 @@ pub enum NumericOperator {
     Mul,
     /// Binary element-wise division of two arrays or of two scalars.
     Div,
+    /// Binary element-wise division that yields 0 instead of erroring when the right-hand-side
+    /// is a non-null zero. Integer overflow still errs, matching [`NumericOperator::Div`].
+    SafeDiv,
 }
 
 impl fmt::Display for NumericOperator {
@@ -33,6 +36,7 @@ impl From<NumericOperator> for crate::scalar_fn::fns::operators::Operator {
             NumericOperator::Sub => crate::scalar_fn::fns::operators::Operator::Sub,
             NumericOperator::Mul => crate::scalar_fn::fns::operators::Operator::Mul,
             NumericOperator::Div => crate::scalar_fn::fns::operators::Operator::Div,
+            NumericOperator::SafeDiv => crate::scalar_fn::fns::operators::Operator::SafeDiv,
         }
     }
 }
diff --git a/vortex-array/src/scalar/typed_view/primitive/scalar.rs b/vortex-array/src/scalar/typed_view/primitive/scalar.rs
index e3981de8dd3..0e41f9e980e 100644
--- a/vortex-array/src/scalar/typed_view/primitive/scalar.rs
+++ b/vortex-array/src/scalar/typed_view/primitive/scalar.rs
@@ -15,6 +15,7 @@ use num_traits::CheckedAdd;
 use num_traits::CheckedDiv;
 use num_traits::CheckedMul;
 use num_traits::CheckedSub;
+use num_traits::Zero;
 use vortex_error::VortexError;
 use vortex_error::VortexExpect;
 use vortex_error::VortexResult;
@@ -339,6 +340,9 @@ impl<'a> PrimitiveScalar<'a> {
                         NumericOperator::Sub => Some(lhs - rhs),
                         NumericOperator::Mul => Some(lhs * rhs),
                         NumericOperator::Div => Some(lhs / rhs),
+                        NumericOperator::SafeDiv => {
+                            Some(if rhs == P::zero() { P::zero() } else { lhs / rhs })
+                        }
                     }
                 };
                 Some(Self { dtype: result_dtype, ptype, pvalue: value_or_null.map(PValue::from) })
@@ -373,6 +377,13 @@ impl<'a> PrimitiveScalar<'a> {
                 NumericOperator::Sub => lhs.checked_sub(&rhs).map(Some),
                 NumericOperator::Mul => lhs.checked_mul(&rhs).map(Some),
                 NumericOperator::Div => lhs.checked_div(&rhs).map(Some),
+                NumericOperator::SafeDiv => {
+                    if rhs == P::zero() {
+                        Some(Some(P::zero()))
+                    } else {
+                        lhs.checked_div(&rhs).map(Some)
+                    }
+                }
             },
         };
 
diff --git a/vortex-array/src/scalar/typed_view/primitive/tests.rs b/vortex-array/src/scalar/typed_view/primitive/tests.rs
index b0c0063a3df..a86aaaa8264 100644
--- a/vortex-array/src/scalar/typed_view/primitive/tests.rs
+++ b/vortex-array/src/scalar/typed_view/primitive/tests.rs
@@ -734,3 +734,73 @@ fn test_f16_nans_equal() {
     let nan3 = f16::from_f16(nan1).unwrap();
     assert_eq!(nan1.to_bits(), nan3.to_bits(),);
 }
+
+#[test]
+fn test_checked_binary_numeric_safe_div_integer() {
+    let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
+    let value1 = ScalarValue::Primitive(PValue::I32(20));
+    let value2 = ScalarValue::Primitive(PValue::I32(4));
+    let scalar1 = PrimitiveScalar::try_new(&dtype, Some(&value1)).unwrap();
+    let scalar2 = PrimitiveScalar::try_new(&dtype, Some(&value2)).unwrap();
+
+    let result = scalar1
+        .checked_binary_numeric(&scalar2, NumericOperator::SafeDiv)
+        .unwrap();
+    assert_eq!(result.typed_value::<i32>(), Some(5));
+}
+
+#[test]
+fn test_checked_binary_numeric_safe_div_integer_by_zero() {
+    let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
+    let value1 = ScalarValue::Primitive(PValue::I32(10));
+    let value2 = ScalarValue::Primitive(PValue::I32(0));
+    let scalar1 = PrimitiveScalar::try_new(&dtype, Some(&value1)).unwrap();
+    let scalar2 = PrimitiveScalar::try_new(&dtype, Some(&value2)).unwrap();
+
+    let result = scalar1
+        .checked_binary_numeric(&scalar2, NumericOperator::SafeDiv)
+        .unwrap();
+    assert_eq!(result.typed_value::<i32>(), Some(0));
+}
+
+#[test]
+fn test_checked_binary_numeric_safe_div_integer_overflow_still_errs() {
+    // i32::MIN / -1 overflows; SafeDiv preserves that behavior (returns None).
+    let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
+    let value1 = ScalarValue::Primitive(PValue::I32(i32::MIN));
+    let value2 = ScalarValue::Primitive(PValue::I32(-1));
+    let scalar1 = PrimitiveScalar::try_new(&dtype, Some(&value1)).unwrap();
+    let scalar2 = PrimitiveScalar::try_new(&dtype, Some(&value2)).unwrap();
+
+    let result = scalar1.checked_binary_numeric(&scalar2, NumericOperator::SafeDiv);
+    assert!(result.is_none());
+}
+
+#[test]
+fn test_checked_binary_numeric_safe_div_float_by_zero() {
+    // Float SafeDiv by zero yields 0.0, not Inf or NaN.
+    let dtype = DType::Primitive(PType::F32, Nullability::NonNullable);
+    let value1 = ScalarValue::Primitive(PValue::F32(1.0));
+    let value2 = ScalarValue::Primitive(PValue::F32(0.0));
+    let scalar1 = PrimitiveScalar::try_new(&dtype, Some(&value1)).unwrap();
+    let scalar2 = PrimitiveScalar::try_new(&dtype, Some(&value2)).unwrap();
+
+    let result = scalar1
+        .checked_binary_numeric(&scalar2, NumericOperator::SafeDiv)
+        .unwrap();
+    assert_eq!(result.typed_value::<f32>(), Some(0.0));
+}
+
+#[test]
+fn test_checked_binary_numeric_safe_div_null_propagates() {
+    // Null on either side yields null.
+    let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
+    let null_scalar = PrimitiveScalar::try_new(&dtype, None).unwrap();
+    let value = ScalarValue::Primitive(PValue::I32(0));
+    let zero_scalar = PrimitiveScalar::try_new(&dtype, Some(&value)).unwrap();
+
+    let result = null_scalar
+        .checked_binary_numeric(&zero_scalar, NumericOperator::SafeDiv)
+        .unwrap();
+    assert_eq!(result.typed_value::<i32>(), None);
+}
diff --git a/vortex-array/src/scalar_fn/fns/binary/mod.rs b/vortex-array/src/scalar_fn/fns/binary/mod.rs
index af81ef0ca28..7b279153efd 100644
--- a/vortex-array/src/scalar_fn/fns/binary/mod.rs
+++ b/vortex-array/src/scalar_fn/fns/binary/mod.rs
@@ -142,7 +142,7 @@ impl ScalarFnVTable for Binary {
         &self,
         op: &Operator,
         args: &dyn ExecutionArgs,
-        _ctx: &mut ExecutionCtx,
+        ctx: &mut ExecutionCtx,
     ) -> VortexResult<ArrayRef> {
         let lhs = args.get(0)?;
         let rhs = args.get(1)?;
@@ -156,10 +156,11 @@ impl ScalarFnVTable for Binary {
             Operator::Gte => execute_compare(&lhs, &rhs, CompareOperator::Gte),
             Operator::And => execute_boolean(&lhs, &rhs, Operator::And),
             Operator::Or => execute_boolean(&lhs, &rhs, Operator::Or),
-            Operator::Add => execute_numeric(&lhs, &rhs, NumericOperator::Add),
-            Operator::Sub => execute_numeric(&lhs, &rhs, NumericOperator::Sub),
-            Operator::Mul => execute_numeric(&lhs, &rhs, NumericOperator::Mul),
-            Operator::Div => execute_numeric(&lhs, &rhs, NumericOperator::Div),
+            Operator::Add => execute_numeric(&lhs, &rhs, NumericOperator::Add, ctx),
+            Operator::Sub => execute_numeric(&lhs, &rhs, NumericOperator::Sub, ctx),
+            Operator::Mul => execute_numeric(&lhs, &rhs, NumericOperator::Mul, ctx),
+            Operator::Div => execute_numeric(&lhs, &rhs, NumericOperator::Div, ctx),
+            Operator::SafeDiv => execute_numeric(&lhs, &rhs, NumericOperator::SafeDiv, ctx),
         }
     }
 
@@ -263,7 +264,9 @@ impl ScalarFnVTable for Binary {
                 lhs.stat_falsification(catalog)?,
                 rhs.stat_falsification(catalog)?,
             )),
-            Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => None,
+            Operator::Add | Operator::Sub | Operator::Mul | Operator::Div | Operator::SafeDiv => {
+                None
+            }
         }
     }
 
diff --git a/vortex-array/src/scalar_fn/fns/binary/numeric.rs b/vortex-array/src/scalar_fn/fns/binary/numeric.rs
index d48b3e230ff..56bdf0de522 100644
--- a/vortex-array/src/scalar_fn/fns/binary/numeric.rs
+++ b/vortex-array/src/scalar_fn/fns/binary/numeric.rs
@@ -1,29 +1,47 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: Copyright the Vortex contributors
 
+use num_traits::CheckedDiv;
+use vortex_buffer::BufferMut;
 use vortex_error::VortexResult;
+use vortex_error::vortex_bail;
+use vortex_error::vortex_err;
 
 use crate::ArrayRef;
+use crate::ExecutionCtx;
 use crate::IntoArray;
+use crate::arrays::BoolArray;
 use crate::arrays::Constant;
 use crate::arrays::ConstantArray;
+use crate::arrays::PrimitiveArray;
 use crate::arrow::Datum;
 use crate::arrow::from_arrow_array_with_len;
+#[expect(deprecated)]
+use crate::canonical::ToCanonical;
+use crate::dtype::DType;
+use crate::dtype::NativePType;
+use crate::match_each_native_ptype;
 use crate::scalar::NumericOperator;
+use crate::validity::Validity;
 
 /// Execute a numeric operation between two arrays.
 ///
 /// This is the entry point for numeric operations from the binary expression.
-/// Handles constant-constant directly, otherwise falls back to Arrow.
+/// Handles constant-constant directly, otherwise falls back to Arrow (or the
+/// dedicated SafeDiv kernel).
 pub(crate) fn execute_numeric(
     lhs: &ArrayRef,
     rhs: &ArrayRef,
     op: NumericOperator,
+    ctx: &mut ExecutionCtx,
 ) -> VortexResult<ArrayRef> {
     if let Some(result) = constant_numeric(lhs, rhs, op)? {
         return Ok(result);
     }
-    arrow_numeric(lhs, rhs, op)
+    match op {
+        NumericOperator::SafeDiv => arrow_safe_div(lhs, rhs, ctx),
+        _ => arrow_numeric(lhs, rhs, op),
+    }
 }
 
 /// Implementation of numeric operations using the Arrow crate.
@@ -43,11 +61,84 @@ pub(crate) fn arrow_numeric(
         NumericOperator::Sub => arrow_arith::numeric::sub(&left, &right)?,
         NumericOperator::Mul => arrow_arith::numeric::mul(&left, &right)?,
         NumericOperator::Div => arrow_arith::numeric::div(&left, &right)?,
+        NumericOperator::SafeDiv => {
+            unreachable!("SafeDiv is dispatched to arrow_safe_div in execute_numeric")
+        }
     };
 
     from_arrow_array_with_len(array.as_ref(), len, nullable)
 }
 
+/// Element-wise SafeDiv kernel for primitive numeric arrays.
+///
+/// Single-pass hand-written kernel: integer `x / 0` yields `0` (not an error), float `x / 0.0`
+/// yields `0.0` (not `Inf`/`NaN`). Integer overflow (e.g. `i32::MIN / -1`) still errs, matching
+/// [`NumericOperator::Div`]. Nulls propagate from either side.
+fn arrow_safe_div(
+    lhs: &ArrayRef,
+    rhs: &ArrayRef,
+    ctx: &mut ExecutionCtx,
+) -> VortexResult<ArrayRef> {
+    if !matches!(lhs.dtype(), DType::Primitive(..)) {
+        vortex_bail!(
+            "safe_div is only implemented for primitive arrays, got {}",
+            lhs.dtype()
+        );
+    }
+
+    #[expect(deprecated)]
+    let lhs = lhs.to_primitive();
+    #[expect(deprecated)]
+    let rhs = rhs.to_primitive();
+
+    // `Validity::and` on two `Validity::Array` variants returns a lazy expression; materialize
+    // it here so downstream reads see the combined bitmap rather than the unevaluated AND node.
+    let validity = match lhs.validity()?.and(rhs.validity()?)? {
+        Validity::Array(v) => Validity::Array(v.execute::<BoolArray>(ctx)?.into_array()),
+        other => other,
+    };
+    let ptype = lhs.ptype();
+
+    match_each_native_ptype!(ptype,
+        integral: |T| {
+            safe_div_integral::<T>(lhs.as_slice::<T>(), rhs.as_slice::<T>(), validity)
+        },
+        floating: |T| {
+            Ok(safe_div_floating::<T>(lhs.as_slice::<T>(), rhs.as_slice::<T>(), validity))
+        }
+    )
+}
+
+fn safe_div_integral<T: NativePType + CheckedDiv>(
+    lhs: &[T],
+    rhs: &[T],
+    validity: Validity,
+) -> VortexResult<ArrayRef> {
+    let zero = T::zero();
+    let mut buffer = BufferMut::<T>::with_capacity(lhs.len());
+    for (l, r) in lhs.iter().zip(rhs) {
+        let value = if *r == zero {
+            zero
+        } else {
+            l.checked_div(r)
+                .ok_or_else(|| vortex_err!("integer overflow in safe_div"))?
+        };
+        buffer.push(value);
+    }
+    Ok(PrimitiveArray::new(buffer.freeze(), validity).into_array())
+}
+
+fn safe_div_floating<T: NativePType>(lhs: &[T], rhs: &[T], validity: Validity) -> ArrayRef {
+    let zero = T::zero();
+    let buffer = BufferMut::<T>::from_trusted_len_iter(
+        lhs.iter()
+            .zip(rhs)
+            .map(|(&l, &r)| if r == zero { zero } else { l / r }),
+    )
+    .freeze();
+    PrimitiveArray::new(buffer, validity).into_array()
+}
+
 fn constant_numeric(
     lhs: &ArrayRef,
     rhs: &ArrayRef,
@@ -135,4 +226,51 @@ mod test {
         let _results = sub_scalar(&values, 1.0f32).unwrap();
         let _results = sub_scalar(&values, f32::MAX).unwrap();
     }
+
+    fn safe_div(lhs: ArrayRef, rhs: ArrayRef) -> VortexResult<ArrayRef> {
+        lhs.binary(rhs, Operator::SafeDiv)
+            .and_then(|a| {
+                a.execute::<RecursiveCanonical>(&mut LEGACY_SESSION.create_execution_ctx())
+            })
+            .map(|a| a.0.into_array())
+    }
+
+    #[test]
+    fn test_safe_div_integer_by_zero_yields_zero() {
+        let lhs = buffer![10i32, 20, 30].into_array();
+        let rhs = buffer![2i32, 0, 5].into_array();
+        let result = safe_div(lhs, rhs).unwrap();
+        assert_arrays_eq!(result, PrimitiveArray::from_iter([5i32, 0, 6]));
+    }
+
+    #[test]
+    fn test_safe_div_float_by_zero_is_zero_not_inf() {
+        let lhs = buffer![1.0f64, 2.0, 3.0].into_array();
+        let rhs = buffer![2.0f64, 0.0, 6.0].into_array();
+        let result = safe_div(lhs, rhs).unwrap();
+        assert_arrays_eq!(result, PrimitiveArray::from_iter([0.5f64, 0.0, 0.5]));
+    }
+
+    #[test]
+    fn test_safe_div_nullable_propagation() {
+        let lhs =
+            PrimitiveArray::from_option_iter([Some(10i32), Some(20), None, Some(40)]).into_array();
+        let rhs =
+            PrimitiveArray::from_option_iter([Some(2i32), Some(0), Some(0), None]).into_array();
+        // Expected: 10/2=5, 20/0=0 (safe), null lhs -> null, null rhs -> null.
+        let result = safe_div(lhs, rhs).unwrap();
+        assert_arrays_eq!(
+            result,
+            PrimitiveArray::from_option_iter([Some(5i32), Some(0), None, None])
+        );
+    }
+
+    #[test]
+    fn test_safe_div_constant_zero_rhs() {
+        // Constant-constant fast path: checked_binary_numeric in constant_numeric().
+        let lhs = buffer![7i32, 8, 9].into_array();
+        let rhs = ConstantArray::new(0i32, lhs.len()).into_array();
+        let result = safe_div(lhs, rhs).unwrap();
+        assert_arrays_eq!(result, PrimitiveArray::from_iter([0i32, 0, 0]));
+    }
 }
diff --git a/vortex-array/src/scalar_fn/fns/operators.rs b/vortex-array/src/scalar_fn/fns/operators.rs
index 498c9c67307..6362fd1a748 100644
--- a/vortex-array/src/scalar_fn/fns/operators.rs
+++ b/vortex-array/src/scalar_fn/fns/operators.rs
@@ -48,6 +48,12 @@ pub enum Operator {
     Mul,
     /// Divide the left side by the right side
     Div,
+    /// Divide the left side by the right side, returning 0 when the right side is 0.
+    ///
+    /// Null propagation matches [`Operator::Div`]: if either input is null, the result is null.
+    /// Unlike [`Operator::Div`], a non-null zero divisor yields a non-null 0 result rather than
+    /// an error. Integer overflow (e.g. `i32::MIN / -1`) still errs, matching [`Operator::Div`].
+    SafeDiv,
 }
 
 impl From<Operator> for i32 {
@@ -72,6 +78,7 @@ impl From<Operator> for BinaryOp {
             Operator::Sub => BinaryOp::Sub,
             Operator::Mul => BinaryOp::Mul,
             Operator::Div => BinaryOp::Div,
+            Operator::SafeDiv => BinaryOp::SafeDiv,
         }
     }
 }
@@ -99,6 +106,7 @@ impl From<BinaryOp> for Operator {
             BinaryOp::Sub => Operator::Sub,
             BinaryOp::Mul => Operator::Mul,
             BinaryOp::Div => Operator::Div,
+            BinaryOp::SafeDiv => Operator::SafeDiv,
         }
     }
 }
@@ -118,6 +126,7 @@ impl Display for Operator {
             Operator::Sub => "-",
             Operator::Mul => "*",
             Operator::Div => "/",
+            Operator::SafeDiv => "//",
         };
         Display::fmt(display, f)
     }
@@ -137,7 +146,8 @@ impl Operator {
             | Operator::Add
             | Operator::Sub
             | Operator::Mul
-            | Operator::Div => None,
+            | Operator::Div
+            | Operator::SafeDiv => None,
         }
     }
 
@@ -162,12 +172,15 @@ impl Operator {
             Operator::Or => Some(Operator::Or),
             Operator::Add => Some(Operator::Add),
             Operator::Mul => Some(Operator::Mul),
-            Operator::Sub | Operator::Div => None,
+            Operator::Sub | Operator::Div | Operator::SafeDiv => None,
         }
     }
 
     pub fn is_arithmetic(&self) -> bool {
-        matches!(self, Self::Add | Self::Sub | Self::Mul | Self::Div)
+        matches!(
+            self,
+            Self::Add | Self::Sub | Self::Mul | Self::Div | Self::SafeDiv
+        )
     }
 
     pub fn is_comparison(&self) -> bool {
diff --git a/vortex-ffi/src/expression.rs b/vortex-ffi/src/expression.rs
index 442f9325959..20e268add5b 100644
--- a/vortex-ffi/src/expression.rs
+++ b/vortex-ffi/src/expression.rs
@@ -158,6 +158,9 @@ pub enum vx_binary_operator {
     VX_OPERATOR_MUL = 10,
     /// Divide the left side by the right side
     VX_OPERATOR_DIV = 11,
+    /// Divide the left side by the right side, returning 0 when the right side is 0.
+    /// Integer overflow still errors, matching VX_OPERATOR_DIV.
+    VX_OPERATOR_SAFE_DIV = 12,
 }
 
 impl From<vx_binary_operator> for Operator {
@@ -175,6 +178,7 @@ impl From<vx_binary_operator> for Operator {
             vx_binary_operator::VX_OPERATOR_SUB => Operator::Sub,
             vx_binary_operator::VX_OPERATOR_MUL => Operator::Mul,
             vx_binary_operator::VX_OPERATOR_DIV => Operator::Div,
+            vx_binary_operator::VX_OPERATOR_SAFE_DIV => Operator::SafeDiv,
         }
     }
 }
diff --git a/vortex-proto/proto/expr.proto b/vortex-proto/proto/expr.proto
index 73ba7209a15..c65c2f34824 100644
--- a/vortex-proto/proto/expr.proto
+++ b/vortex-proto/proto/expr.proto
@@ -59,6 +59,7 @@ message BinaryOpts {
     Sub = 9;
     Mul = 10;
     Div = 11;
+    SafeDiv = 12;
   }
 }
 
diff --git a/vortex-proto/src/generated/vortex.expr.rs b/vortex-proto/src/generated/vortex.expr.rs
index 9c7ddb1d90c..31bcca5bf28 100644
--- a/vortex-proto/src/generated/vortex.expr.rs
+++ b/vortex-proto/src/generated/vortex.expr.rs
@@ -72,6 +72,7 @@ pub mod binary_opts {
         Sub = 9,
         Mul = 10,
         Div = 11,
+        SafeDiv = 12,
     }
     impl BinaryOp {
         /// String value of the enum field names used in the ProtoBuf definition.
@@ -92,6 +93,7 @@ pub mod binary_opts {
                 Self::Sub => "Sub",
                 Self::Mul => "Mul",
                 Self::Div => "Div",
+                Self::SafeDiv => "SafeDiv",
             }
         }
         /// Creates an enum from field names used in the ProtoBuf definition.
@@ -109,6 +111,7 @@ pub mod binary_opts {
                 "Sub" => Some(Self::Sub),
                 "Mul" => Some(Self::Mul),
                 "Div" => Some(Self::Div),
+                "SafeDiv" => Some(Self::SafeDiv),
                 _ => None,
             }
         }
