You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@asterixdb.apache.org by AsterixDB Code Review <do...@asterix-gerrit.ics.uci.edu> on 2026/09/18 08:16:40 UTC

[L] Change in asterixdb[master]: PLEASE EDIT to provide a meaningful commit message!

From Calvin Thomas Dani <ca...@gmail.com>:

Calvin Thomas Dani has uploaded this change for review. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21823?usp=email )


Change subject: PLEASE EDIT to provide a meaningful commit message!
......................................................................

PLEASE EDIT to provide a meaningful commit message!

The following commits from your working branch will be included:

commit fe942e864d21f0766e3d37cf3297457e5a895fe1
Author: Calvin Thomas Dani <ca...@gmail.com>
Date:   Fri Sep 18 01:02:08 2026 -0700

    [NO ISSUE][COMP] Add normalize_vector for unit-length embeddings

    Cosine workflows need the L2-normalized form of a vector without
    expressing the division in SQL++. Couchbase's NORMALIZE_VECTOR and its
    aliases resolve to the same builtin; empty, zero, and non-finite inputs
    return null with a warning, matching cosine_similarity.

    Co-Authored-By: Grok 4.6 <no...@x.ai>
    Co-authored-by: Cursor <cu...@cursor.com>

# Do not delete the following line!
Change-Id: Ib2b71894055420fa17fcd4cead753bc8f8bc5604
---
M asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/VectorQueries.xml
A asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/normalize-vector/normalize-vector.1.query.sqlpp
A asterixdb/asterix-app/src/test/resources/runtimets/results/vector/normalize-vector/normalize-vector.1.adm
M asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/CommonFunctionMapUtil.java
M asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
A asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/vector/NormalizeVectorDescriptor.java
M asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
M asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/utils/VectorDistanceCalculation.java
A asterixdb/asterix-runtime/src/test/java/org/apache/asterix/runtime/utils/VectorDistanceCalculationNormalizeTest.java
M hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
10 files changed, 342 insertions(+), 1 deletion(-)



  git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb refs/changes/23/21823/1

diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/VectorQueries.xml b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/VectorQueries.xml
index 37a7f27..12061d2 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/VectorQueries.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/VectorQueries.xml
@@ -22,6 +22,23 @@
             <output-dir compare="Clean-JSON">distance-functions</output-dir>
         </compilation-unit>
     </test-case>
+    <test-case FilePath="vector" check-warnings="true">
+        <compilation-unit name="normalize-vector">
+            <output-dir compare="Clean-JSON">normalize-vector</output-dir>
+            <!-- Empty list. -->
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: normalize-vector expects a non-empty numeric list</expected-warn>
+            <!-- Non-list argument. -->
+            <expected-warn>Type mismatch: function normalize-vector expects its 1st input parameter to be of type array or multiset, but the actual input type is string</expected-warn>
+            <!-- Mixed, zero, NaN, INF, null element, nested, all-string: non-numeric → NaN → zero/non-finite path. -->
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+            <expected-warn>Function 'normalize-vector' failed to evaluate because: cannot normalize a zero-length or non-finite vector</expected-warn>
+        </compilation-unit>
+    </test-case>
     <test-case FilePath="vector">
         <compilation-unit name="create-index-vtree">
             <output-dir compare="Clean-JSON">create-index-vtree</output-dir>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/normalize-vector/normalize-vector.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/normalize-vector/normalize-vector.1.query.sqlpp
new file mode 100644
index 0000000..e1a2ff2
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/vector/normalize-vector/normalize-vector.1.query.sqlpp
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+// param max-warnings:json=100
+//
+// normalize_vector(v) and Couchbase aliases (NORMALISE_VECTOR, VECTOR_NORMALIZE, VECTOR_NORMALISE).
+// Non-happy paths mirror isvector's coverage (mixed, nested, empty, non-list, null, missing) but
+// return null (with a warning) rather than false, matching cosine_similarity's zero-norm path.
+// "m_missing" is absent.
+{
+  "a_1234":      normalize_vector([1, 2, 3, 4]),
+  "b_34":        normalize_vector([3, 4]),
+  "c_unit":      normalize_vector([0, 1]),
+  "d_ints":      normalize_vector([1, 0, 0]),
+  "e_alias_ise": normalise_vector([0, 1]),
+  "f_alias_vn":  vector_normalize([0, 1]),
+  "g_alias_vne": vector_normalise([0, 1]),
+  "h_alias_cmp": normalizevector([0, 1]),
+  "i_hyphen":    `normalize-vector`([0, 1]),
+  "j_neg":       normalize_vector([-3, 4]),
+  "k_one":       normalize_vector([5]),
+  "l_null":      normalize_vector(null),
+  "m_missing":   normalize_vector(missing),
+  "n_empty":     normalize_vector([]),
+  "o_not_list":  normalize_vector("x"),
+  "p_mixed":     normalize_vector([1, "s"]),
+  "q_zero":      normalize_vector([0, 0, 0]),
+  "r_nan":       normalize_vector([double("NaN"), 1]),
+  "s_inf":       normalize_vector([double("INF"), 0]),
+  "t_null_elem": normalize_vector([1, null]),
+  "u_nested":    normalize_vector([0.2, 0.1, [1], 0.3]),
+  "v_all_string": normalize_vector(["a", "b", "c", "d"])
+};
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/vector/normalize-vector/normalize-vector.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/vector/normalize-vector/normalize-vector.1.adm
new file mode 100644
index 0000000..bc89aa3
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/vector/normalize-vector/normalize-vector.1.adm
@@ -0,0 +1 @@
+{ "a_1234": [ 0.18257418583505536, 0.3651483716701107, 0.5477225575051661, 0.7302967433402214 ], "b_34": [ 0.6000000000000001, 0.8 ], "c_unit": [ 0.0, 1.0 ], "d_ints": [ 1.0, 0.0, 0.0 ], "e_alias_ise": [ 0.0, 1.0 ], "f_alias_vn": [ 0.0, 1.0 ], "g_alias_vne": [ 0.0, 1.0 ], "h_alias_cmp": [ 0.0, 1.0 ], "i_hyphen": [ 0.0, 1.0 ], "j_neg": [ -0.6000000000000001, 0.8 ], "k_one": [ 1.0 ], "l_null": null, "n_empty": null, "o_not_list": null, "p_mixed": null, "q_zero": null, "r_nan": null, "s_inf": null, "t_null_elem": null, "u_nested": null, "v_all_string": null }
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/CommonFunctionMapUtil.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/CommonFunctionMapUtil.java
index d3d8ae1..e864ff9 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/CommonFunctionMapUtil.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/CommonFunctionMapUtil.java
@@ -168,7 +168,8 @@
         addFunctionMapping("variance_pop", "var_pop");
 
         // Vector functions — canonical SQL++ names: euclidean_distance, euclidean_squared_distance,
-        // cosine_similarity, dot_product, vector_distance (compile-time rewrite to concrete metric)
+        // cosine_similarity, dot_product, vector_distance (compile-time rewrite to concrete metric),
+        // normalize_vector (underscore → hyphen). Couchbase / British / compressed spellings below.
         addFunctionMapping("approx_vector_distance", "ann-distance");
         // Compatibility aliases
         addFunctionMapping("l2_distance", "euclidean-distance");
@@ -176,6 +177,10 @@
         // cosine-distance is a private builtin (use vector_distance / ann_distance for cosine), so there
         // is deliberately no public cosine_distance alias — mapping one resolves to a private function.
         addFunctionMapping("dot_product", "dot-product");
+        addFunctionMapping("normalise_vector", "normalize-vector");
+        addFunctionMapping("vector_normalize", "normalize-vector");
+        addFunctionMapping("vector_normalise", "normalize-vector");
+        addFunctionMapping("normalizevector", "normalize-vector");
 
         // OGC SFA geospatial function aliases.
         // Many GIS dialects use CamelCase (e.g. ST_IsEmpty) which lowercases to
diff --git a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
index 47fceca..c87d1a3 100644
--- a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
+++ b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
@@ -1226,6 +1226,7 @@
     public static final FunctionIdentifier COSINE_DISTANCE = FunctionConstants.newAsterix("cosine-distance", 2);
     public static final FunctionIdentifier DOT_DISTANCE = FunctionConstants.newAsterix("dot-distance", 2);
     public static final FunctionIdentifier VECTOR_DISTANCE = FunctionConstants.newAsterix("vector-distance", 3);
+    public static final FunctionIdentifier NORMALIZE_VECTOR = FunctionConstants.newAsterix("normalize-vector", 1);
 
     // Vector search functions
     public static final FunctionIdentifier ANN_DISTANCE =
@@ -1980,6 +1981,7 @@
         addPrivateFunction(COSINE_DISTANCE, ADoubleTypeComputer.INSTANCE, true);
         addPrivateFunction(DOT_DISTANCE, ADoubleTypeComputer.INSTANCE, true);
         addFunction(VECTOR_DISTANCE, ADoubleTypeComputer.INSTANCE, true);
+        addFunction(NORMALIZE_VECTOR, NullableOrderedListOfADoubleTypeComputer.INSTANCE, true);
 
         // Ann functions
         addFunction(ANN_DISTANCE, ADoubleTypeComputer.INSTANCE, true);
diff --git a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/vector/NormalizeVectorDescriptor.java b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/vector/NormalizeVectorDescriptor.java
new file mode 100644
index 0000000..374b0cb
--- /dev/null
+++ b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/vector/NormalizeVectorDescriptor.java
@@ -0,0 +1,157 @@
+/*
+ * 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.asterix.runtime.evaluators.functions.vector;
+
+import java.io.DataOutput;
+import java.io.IOException;
+
+import org.apache.asterix.builders.OrderedListBuilder;
+import org.apache.asterix.common.annotations.MissingNullInOutFunction;
+import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
+import org.apache.asterix.om.base.ADouble;
+import org.apache.asterix.om.base.AMutableDouble;
+import org.apache.asterix.om.exceptions.ExceptionUtil;
+import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.asterix.om.functions.IFunctionDescriptorFactory;
+import org.apache.asterix.om.types.AOrderedListType;
+import org.apache.asterix.om.types.ATypeTag;
+import org.apache.asterix.om.types.BuiltinType;
+import org.apache.asterix.runtime.evaluators.base.AbstractScalarFunctionDynamicDescriptor;
+import org.apache.asterix.runtime.evaluators.common.ListAccessor;
+import org.apache.asterix.runtime.evaluators.functions.PointableHelper;
+import org.apache.asterix.runtime.utils.VectorDistanceCalculation;
+import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluator;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluatorFactory;
+import org.apache.hyracks.api.context.IEvaluatorContext;
+import org.apache.hyracks.api.dataflow.value.ISerializerDeserializer;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.api.exceptions.SourceLocation;
+import org.apache.hyracks.data.std.api.IPointable;
+import org.apache.hyracks.data.std.primitive.VoidPointable;
+import org.apache.hyracks.data.std.util.ArrayBackedValueStorage;
+import org.apache.hyracks.dataflow.common.data.accessors.IFrameTupleReference;
+import org.apache.hyracks.util.annotations.AiProvenance;
+
+/**
+ * {@code normalize_vector(v)} returns the unit-length vector in the same direction as {@code v}.
+ * <p>
+ * Missing and null propagate. An empty list, a non-list, a non-numeric element, a non-finite component,
+ * or a zero vector has no direction to preserve, so the result is null (with a warning), matching
+ * {@code cosine_similarity}'s zero-norm → NaN → null path rather than {@code isvector}'s boolean.
+ */
+@MissingNullInOutFunction
+@AiProvenance(agent = AiProvenance.Agent.GROK_4_6, tool = AiProvenance.Tool.CURSOR, contributionKind = AiProvenance.ContributionKind.GENERATED, notes = "NORMALIZE_VECTOR scalar evaluator")
+public class NormalizeVectorDescriptor extends AbstractScalarFunctionDynamicDescriptor {
+    private static final long serialVersionUID = 1L;
+
+    public static final IFunctionDescriptorFactory FACTORY = NormalizeVectorDescriptor::new;
+
+    @Override
+    public FunctionIdentifier getIdentifier() {
+        return BuiltinFunctions.NORMALIZE_VECTOR;
+    }
+
+    @Override
+    public IScalarEvaluatorFactory createEvaluatorFactory(final IScalarEvaluatorFactory[] args) {
+        return new IScalarEvaluatorFactory() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public IScalarEvaluator createScalarEvaluator(final IEvaluatorContext ctx) throws HyracksDataException {
+                return new NormalizeVectorEvaluator(args, ctx, getIdentifier(), sourceLoc);
+            }
+        };
+    }
+
+    private static final class NormalizeVectorEvaluator implements IScalarEvaluator {
+        private static final AOrderedListType DOUBLE_LIST = new AOrderedListType(BuiltinType.ADOUBLE, null);
+
+        private final ArrayBackedValueStorage resultStorage = new ArrayBackedValueStorage();
+        private final DataOutput out = resultStorage.getDataOutput();
+        private final ArrayBackedValueStorage itemStorage = new ArrayBackedValueStorage();
+        private final IPointable valuePtr = new VoidPointable();
+        private final IScalarEvaluator valueEval;
+        private final VectorListDecoder decoder = new VectorListDecoder();
+        private final ListAccessor listAccessor = new ListAccessor();
+        private final OrderedListBuilder listBuilder = new OrderedListBuilder();
+        private final AMutableDouble aDouble = new AMutableDouble(0);
+        @SuppressWarnings("unchecked")
+        private final ISerializerDeserializer<ADouble> doubleSerde =
+                SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.ADOUBLE);
+        private final IEvaluatorContext ctx;
+        private final FunctionIdentifier funcId;
+        private final SourceLocation sourceLoc;
+        private double[] vector = new double[0];
+
+        private NormalizeVectorEvaluator(IScalarEvaluatorFactory[] args, IEvaluatorContext ctx,
+                FunctionIdentifier funcId, SourceLocation sourceLoc) throws HyracksDataException {
+            this.valueEval = args[0].createScalarEvaluator(ctx);
+            this.ctx = ctx;
+            this.funcId = funcId;
+            this.sourceLoc = sourceLoc;
+        }
+
+        @Override
+        public void evaluate(IFrameTupleReference tuple, IPointable result) throws HyracksDataException {
+            valueEval.evaluate(tuple, valuePtr);
+            if (PointableHelper.checkAndSetMissingOrNull(result, valuePtr)) {
+                return;
+            }
+            resultStorage.reset();
+            try {
+                if (!decoder.checkListType(valuePtr)) {
+                    ExceptionUtil.warnTypeMismatch(ctx, sourceLoc, funcId,
+                            valuePtr.getByteArray()[valuePtr.getStartOffset()], 0,
+                            new byte[] { ATypeTag.SERIALIZED_ORDEREDLIST_TYPE_TAG,
+                                    ATypeTag.SERIALIZED_UNORDEREDLIST_TYPE_TAG });
+                    PointableHelper.setNull(result);
+                    return;
+                }
+                listAccessor.reset(valuePtr.getByteArray(), valuePtr.getStartOffset());
+                int size = listAccessor.size();
+                if (size == 0) {
+                    ExceptionUtil.warnFunctionEvalFailed(ctx, sourceLoc, funcId,
+                            "normalize-vector expects a non-empty numeric list");
+                    PointableHelper.setNull(result);
+                    return;
+                }
+                vector = decoder.createArrayFromList(listAccessor, decoder.ensureDoubleCapacity(vector, size));
+                if (!VectorDistanceCalculation.normalizeInPlace(vector)) {
+                    ExceptionUtil.warnFunctionEvalFailed(ctx, sourceLoc, funcId,
+                            "cannot normalize a zero-length or non-finite vector");
+                    PointableHelper.setNull(result);
+                    return;
+                }
+                listBuilder.reset(DOUBLE_LIST);
+                for (int i = 0; i < vector.length; i++) {
+                    itemStorage.reset();
+                    aDouble.setValue(vector[i]);
+                    doubleSerde.serialize(aDouble, itemStorage.getDataOutput());
+                    listBuilder.addItem(itemStorage);
+                }
+                listBuilder.write(out, true);
+                result.set(resultStorage);
+            } catch (IOException e) {
+                ExceptionUtil.warnFunctionEvalFailed(ctx, sourceLoc, funcId, e.getMessage());
+                PointableHelper.setNull(result);
+            }
+        }
+    }
+}
diff --git a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
index 13cf75a..6ee56f2 100644
--- a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
+++ b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
@@ -661,6 +661,7 @@
 import org.apache.asterix.runtime.evaluators.functions.vector.EuclideanDistanceDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.vector.EuclideanSquaredDistanceDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.vector.NearestCentroidDescriptor;
+import org.apache.asterix.runtime.evaluators.functions.vector.NormalizeVectorDescriptor;
 import org.apache.asterix.runtime.runningaggregates.std.DenseRankRunningAggregateDescriptor;
 import org.apache.asterix.runtime.runningaggregates.std.NtileRunningAggregateDescriptor;
 import org.apache.asterix.runtime.runningaggregates.std.PercentRankRunningAggregateDescriptor;
@@ -1361,6 +1362,7 @@
         fc.add(DotProductDescriptor.FACTORY);
         fc.add(CosineDistanceDescriptor.FACTORY);
         fc.add(DotDistanceDescriptor.FACTORY);
+        fc.add(NormalizeVectorDescriptor.FACTORY);
 
         fc.add(NearestCentroidDescriptor.FACTORY);
 
diff --git a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/utils/VectorDistanceCalculation.java b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/utils/VectorDistanceCalculation.java
index 59c0367..748b34c 100644
--- a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/utils/VectorDistanceCalculation.java
+++ b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/utils/VectorDistanceCalculation.java
@@ -135,6 +135,34 @@
         return Math.sqrt(sum);
     }
 
+    /**
+     * Divides {@code v} by its L2 norm in place. Returns false (and leaves {@code v} unchanged) when the
+     * vector is null, empty, zero-length, or contains a non-finite component — there is no unit vector to
+     * return in those cases.
+     */
+    @AiProvenance(agent = AiProvenance.Agent.GROK_4_6, tool = AiProvenance.Tool.CURSOR, contributionKind = AiProvenance.ContributionKind.GENERATED, notes = "NORMALIZE_VECTOR L2 helper")
+    public static boolean normalizeInPlace(double[] v) {
+        if (v == null || v.length == 0) {
+            return false;
+        }
+        double sum = 0.0;
+        for (int i = 0; i < v.length; i++) {
+            double x = v[i];
+            if (!Double.isFinite(x)) {
+                return false;
+            }
+            sum += x * x;
+        }
+        if (sum == 0.0 || !Double.isFinite(sum)) {
+            return false;
+        }
+        double inv = 1.0 / Math.sqrt(sum);
+        for (int i = 0; i < v.length; i++) {
+            v[i] *= inv;
+        }
+        return true;
+    }
+
     public static double euclideanSquared(double[] a, double[] b) {
         double sum = 0.0;
         for (int i = 0; i < a.length; i++) {
diff --git a/asterixdb/asterix-runtime/src/test/java/org/apache/asterix/runtime/utils/VectorDistanceCalculationNormalizeTest.java b/asterixdb/asterix-runtime/src/test/java/org/apache/asterix/runtime/utils/VectorDistanceCalculationNormalizeTest.java
new file mode 100644
index 0000000..bfd1d8a
--- /dev/null
+++ b/asterixdb/asterix-runtime/src/test/java/org/apache/asterix/runtime/utils/VectorDistanceCalculationNormalizeTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.asterix.runtime.utils;
+
+import org.apache.hyracks.util.annotations.AiProvenance;
+import org.junit.Assert;
+import org.junit.Test;
+
+@AiProvenance(agent = AiProvenance.Agent.GROK_4_6, tool = AiProvenance.Tool.CURSOR, contributionKind = AiProvenance.ContributionKind.TEST_GENERATED, notes = "NORMALIZE_VECTOR L2 helper")
+public class VectorDistanceCalculationNormalizeTest {
+
+    @Test
+    public void fourComponentVector() {
+        double[] v = { 1, 2, 3, 4 };
+        Assert.assertTrue(VectorDistanceCalculation.normalizeInPlace(v));
+        Assert.assertEquals(0.18257418583505536, v[0], 0.0);
+        Assert.assertEquals(0.3651483716701107, v[1], 0.0);
+        Assert.assertEquals(0.5477225575051661, v[2], 0.0);
+        Assert.assertEquals(0.7302967433402214, v[3], 0.0);
+    }
+
+    @Test
+    public void unitAndPythagorean() {
+        double[] unit = { 0, 1 };
+        Assert.assertTrue(VectorDistanceCalculation.normalizeInPlace(unit));
+        Assert.assertArrayEquals(new double[] { 0.0, 1.0 }, unit, 0.0);
+
+        double[] pythagorean = { 3, 4 };
+        Assert.assertTrue(VectorDistanceCalculation.normalizeInPlace(pythagorean));
+        // 3*(1/5) is 1 ulp above 0.6; compare the same way the helper divides.
+        Assert.assertEquals(3.0 / 5.0, pythagorean[0], 1e-15);
+        Assert.assertEquals(4.0 / 5.0, pythagorean[1], 1e-15);
+    }
+
+    @Test
+    public void negativePreservesDirection() {
+        double[] v = { -3, 4 };
+        Assert.assertTrue(VectorDistanceCalculation.normalizeInPlace(v));
+        Assert.assertEquals(-3.0 * (1.0 / 5.0), v[0], 0.0);
+        Assert.assertEquals(4.0 * (1.0 / 5.0), v[1], 0.0);
+    }
+
+    @Test
+    public void rejectsEmptyZeroAndNonFinite() {
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(new double[0]));
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(new double[] { 0, 0, 0 }));
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(new double[] { Double.NaN, 1 }));
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(new double[] { Double.POSITIVE_INFINITY }));
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(null));
+    }
+
+    @Test
+    public void failureLeavesArrayUnchanged() {
+        double[] zero = { 0, 0 };
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(zero));
+        Assert.assertArrayEquals(new double[] { 0, 0 }, zero, 0.0);
+
+        double[] nan = { Double.NaN, 1 };
+        Assert.assertFalse(VectorDistanceCalculation.normalizeInPlace(nan));
+        Assert.assertTrue(Double.isNaN(nan[0]));
+        Assert.assertEquals(1.0, nan[1], 0.0);
+    }
+}
diff --git a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
index 917e6c4..c9aae6c 100644
--- a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
+++ b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
@@ -258,6 +258,7 @@
         // xAI
         // =========================
         GROK_CODE_FAST_1(XAI, "grok-code-fast-1", "Grok Code Fast 1"),
+        GROK_4_6(XAI, "grok-4.6", "Grok 4.6"),
 
         // =========================
         // OSS / local

-- 
To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21823?usp=email
To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: asterixdb
Gerrit-Branch: master
Gerrit-Change-Id: Ib2b71894055420fa17fcd4cead753bc8f8bc5604
Gerrit-Change-Number: 21823
Gerrit-PatchSet: 1
Gerrit-Owner: Calvin Thomas Dani <ca...@gmail.com>