malinjawi commented on code in PR #12647:
URL: https://github.com/apache/gluten/pull/12647#discussion_r4350731833


##########
cpp/velox/jni/VeloxJniWrapper.cc:
##########
@@ -717,6 +717,72 @@ JNIEXPORT jlong JNICALL 
Java_org_apache_gluten_columnarbatch_VeloxColumnarBatchJ
   JNI_METHOD_END(kInvalidObjectHandle)
 }
 
+JNIEXPORT jint JNICALL 
Java_org_apache_gluten_columnarbatch_VeloxColumnarBatchJniWrapper_firstNullColumnIndex(
 // NOLINT
+    JNIEnv* env,
+    jobject wrapper,
+    jlong veloxBatchHandle,
+    jintArray columnOrdinals) {
+  JNI_METHOD_START
+  auto ctx = getRuntime(env, wrapper);
+  auto runtime = dynamic_cast<VeloxRuntime*>(ctx);
+  auto batch = ObjectStore::retrieve<ColumnarBatch>(veloxBatchHandle);
+  auto safeColumnOrdinals = getIntArrayElementsSafe(env, columnOrdinals);
+
+  const auto numColumnsToCheck = safeColumnOrdinals.length();
+  if (numColumnsToCheck == 0 || batch->numRows() == 0) {
+    return -1;
+  }
+
+  auto veloxBatch = 
VeloxColumnarBatch::from(runtime->memoryManager()->getLeafMemoryPool().get(), 
batch);
+  auto rowVector = veloxBatch->getRowVector();
+  const auto rowCount = rowVector->size();
+  const auto columnCount = static_cast<int32_t>(rowVector->childrenSize());
+
+  for (int i = 0; i < numColumnsToCheck; ++i) {
+    const auto ordinal = safeColumnOrdinals.elems()[i];
+    GLUTEN_CHECK(ordinal >= 0 && ordinal < columnCount, "Column ordinal 
overflow when checking Delta invariant");
+  }
+
+  if (rowVector->mayHaveNulls()) {
+    const auto nullCount = rowVector->getNullCount();
+    if (nullCount.has_value()) {
+      if (*nullCount > 0) {
+        return 0;
+      }
+    } else {
+      for (auto row = 0; row < rowCount; ++row) {
+        if (rowVector->isNullAt(row)) {
+          return 0;
+        }
+      }
+    }
+  }
+
+  for (int i = 0; i < numColumnsToCheck; ++i) {
+    const auto ordinal = safeColumnOrdinals.elems()[i];
+    const auto child = rowVector->childAt(ordinal);
+    VELOX_CHECK_NOT_NULL(child, "Column vector is null when checking Delta 
invariant.");
+    if (!child->mayHaveNulls()) {
+      continue;
+    }
+    const auto nullCount = child->getNullCount();
+    if (nullCount.has_value()) {
+      if (*nullCount > 0) {
+        return i;
+      }
+      continue;
+    }

Review Comment:
   Good catch, thanks. You're right that the cached count can't be trusted for 
an invariant check, so in a007c3f94 I dropped `getNullCount()` entirely. Each 
constrained column now goes through a `DecodedVector`, so nulls in the base of 
a dictionary or constant wrapper are included, and the nulls are counted with 
`bits::countNulls`. Row-level nulls are read straight from the RowVector's own 
nulls buffer, since that one is always flat.
   
   I also moved the scan into `VeloxColumnarBatch::firstNullColumnIndex` so it 
could be unit tested. `VeloxColumnarBatchTest` now covers a stale null count of 
0, a dictionary with a null count of 0 over a null base value, a dictionary 
whose indices skip the null, a null constant, a null row, and a child that's 
longer than the batch.
   



##########
cpp/velox/jni/VeloxJniWrapper.cc:
##########
@@ -717,6 +717,72 @@ JNIEXPORT jlong JNICALL 
Java_org_apache_gluten_columnarbatch_VeloxColumnarBatchJ
   JNI_METHOD_END(kInvalidObjectHandle)
 }
 
+JNIEXPORT jint JNICALL 
Java_org_apache_gluten_columnarbatch_VeloxColumnarBatchJniWrapper_firstNullColumnIndex(
 // NOLINT
+    JNIEnv* env,
+    jobject wrapper,
+    jlong veloxBatchHandle,
+    jintArray columnOrdinals) {
+  JNI_METHOD_START
+  auto ctx = getRuntime(env, wrapper);
+  auto runtime = dynamic_cast<VeloxRuntime*>(ctx);
+  auto batch = ObjectStore::retrieve<ColumnarBatch>(veloxBatchHandle);
+  auto safeColumnOrdinals = getIntArrayElementsSafe(env, columnOrdinals);
+
+  const auto numColumnsToCheck = safeColumnOrdinals.length();
+  if (numColumnsToCheck == 0 || batch->numRows() == 0) {
+    return -1;
+  }
+
+  auto veloxBatch = 
VeloxColumnarBatch::from(runtime->memoryManager()->getLeafMemoryPool().get(), 
batch);
+  auto rowVector = veloxBatch->getRowVector();
+  const auto rowCount = rowVector->size();
+  const auto columnCount = static_cast<int32_t>(rowVector->childrenSize());
+
+  for (int i = 0; i < numColumnsToCheck; ++i) {
+    const auto ordinal = safeColumnOrdinals.elems()[i];
+    GLUTEN_CHECK(ordinal >= 0 && ordinal < columnCount, "Column ordinal 
overflow when checking Delta invariant");
+  }
+
+  if (rowVector->mayHaveNulls()) {
+    const auto nullCount = rowVector->getNullCount();
+    if (nullCount.has_value()) {
+      if (*nullCount > 0) {
+        return 0;
+      }
+    } else {
+      for (auto row = 0; row < rowCount; ++row) {
+        if (rowVector->isNullAt(row)) {
+          return 0;
+        }
+      }
+    }
+  }
+
+  for (int i = 0; i < numColumnsToCheck; ++i) {
+    const auto ordinal = safeColumnOrdinals.elems()[i];
+    const auto child = rowVector->childAt(ordinal);
+    VELOX_CHECK_NOT_NULL(child, "Column vector is null when checking Delta 
invariant.");
+    if (!child->mayHaveNulls()) {
+      continue;
+    }
+    const auto nullCount = child->getNullCount();
+    if (nullCount.has_value()) {
+      if (*nullCount > 0) {
+        return i;
+      }
+      continue;
+    }
+    for (auto row = 0; row < rowCount; ++row) {
+      if (child->isNullAt(row)) {
+        return i;
+      }
+    }

Review Comment:
   Done, pretty much your snippet: decode with `allRows`, then 
`bits::countNulls(decoded.nulls(&allRows), 0, numRows)`.
   
   I was curious about the neutral benchmark as well, so I re-ran it and timed 
the JNI call on its own. On a 2M-row append (~490 batches) the check takes 
about 2 ms, both before and after this change. In these pipelines the vectors 
reaching the writer happened to take the old shortcut, so the per-row loop 
wasn't what made the old numbers neutral. Either way the new version is the 
correct one and costs nothing measurable. The native path now comes out about 
the same as an unconstrained write, and the row CHECK fallback is 21–28% 
slower. The numbers are in the description.
   



##########
backends-velox/src-delta33/main/scala/org/apache/spark/sql/delta/files/GlutenDeltaFileFormatWriter.scala:
##########
@@ -225,7 +233,8 @@ object GlutenDeltaFileFormatWriter extends LoggingShims {
         partitionColumns,
         sortColumns,
         orderingMatched,
-        isNativeWritable
+        isNativeWritable,
+        nativeInvariantChecker

Review Comment:
   Agreed, this shouldn't depend on an invariant spread across two files. Both 
writers (`src-delta33` and `src-delta4`) now 
`require(nativeInvariantChecker.isEmpty, ...)` in the `writeFilesOpt.isDefined` 
branch (a007c3f94), so it fails loudly instead of writing unchecked. I didn't 
thread the checker through that overload, because `WriteFilesExec` runs its own 
tasks and there's no iterator there to wrap.
   
   The other half, not building a checker when the plan has a `WriteFilesExec`, 
now lives in one place, `GlutenDeltaInvariantChecker.forWrite` (3da00cd46), 
instead of being repeated in both transactions.
   



##########
backends-velox/src-delta/main/scala/org/apache/spark/sql/delta/constraints/GlutenDeltaInvariantChecker.scala:
##########
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.delta.constraints
+
+import org.apache.gluten.columnarbatch.VeloxColumnarBatches
+import org.apache.gluten.execution.{PlaceholderRow, TerminalRow}
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.delta.constraints.Constraints.NotNull
+import org.apache.spark.sql.delta.schema.{DeltaInvariantViolationException, 
SchemaUtils}
+import org.apache.spark.sql.vectorized.ColumnarBatch
+
+/**
+ * Native write-time invariant checker for constraints that can be validated 
without converting
+ * Velox batches back to Spark rows.
+ *
+ * Violations are detected natively from batch null metadata (no per-row 
expression evaluation, no
+ * extra operator in the write plan) but thrown on the JVM side, preserving 
Delta's typed
+ * [[DeltaInvariantViolationException]] error contract. Offloading Delta's 
CheckDeltaInvariant
+ * expression to Velox instead would lose that contract: exceptions raised 
inside Velox surface only
+ * as GlutenException with a message string. CHECK constraints and nested NOT 
NULL constraints fall
+ * back to Delta's row-based DeltaInvariantCheckerExec.
+ */
+private[delta] case class GlutenDeltaInvariantChecker private (
+    notNullConstraints: Seq[(Int, NotNull)])
+  extends Serializable {
+
+  @transient private lazy val columnOrdinals: Array[Int] =
+    notNullConstraints.map(_._1).toArray
+
+  def wrap(rows: Iterator[InternalRow]): Iterator[InternalRow] = {
+    rows.map {
+      row =>
+        check(row)
+        row
+    }
+  }
+
+  private def check(row: InternalRow): Unit = row match {
+    case _: PlaceholderRow =>
+    case terminal: TerminalRow => check(terminal.batch())
+    case other => checkRow(other)
+  }
+
+  private def check(batch: ColumnarBatch): Unit = {
+    val failedConstraintIndex = 
VeloxColumnarBatches.firstNullColumnIndex(batch, columnOrdinals)
+    if (failedConstraintIndex >= 0) {
+      throw 
DeltaInvariantViolationException(notNullConstraints(failedConstraintIndex)._2)
+    }
+  }
+
+  private def checkRow(row: InternalRow): Unit = {
+    var i = 0
+    while (i < notNullConstraints.size) {
+      val (ordinal, constraint) = notNullConstraints(i)
+      if (row.isNullAt(ordinal)) {
+        throw DeltaInvariantViolationException(constraint)
+      }
+      i += 1
+    }
+  }

Review Comment:
   Fixed in a007c3f94. The checker now takes `columnOrdinals: Array[Int]` and 
`constraints: Array[NotNull]` as parallel arrays, so both the batch path and 
the row path are plain array lookups. I also added 
`GlutenDeltaInvariantCheckerSuite` for the row path; it doesn't need the native 
library.
   



##########
backends-velox/src-delta/main/scala/org/apache/spark/sql/delta/constraints/GlutenDeltaInvariantChecker.scala:
##########
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.delta.constraints

Review Comment:
   Good point, moved. There's a single copy now, in 
`backends-velox/src-delta/main/scala/org/apache/spark/sql/delta/constraints/GlutenDeltaInvariantChecker.scala`
 (a007c3f94). Since #13271 the 4.x side is `src-delta4`, so this one file 
covers Delta 3.3 and 4.4. It also compiles against Delta 2.4 for the Spark 3.4 
build, where it isn't used.
   
   The same drift concern applied to the code deciding whether to use the 
checker, which was still duplicated in both `GlutenOptimisticTransaction`s, so 
that moved into the same file as `forWrite` (3da00cd46).
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to