This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch PHOENIX-7998
in repository https://gitbox.apache.org/repos/asf/phoenix.git

commit 84962101dd5ad5dec842bc24e54a8ad28a4512c6
Author: Andrew Purtell <[email protected]>
AuthorDate: Thu Sep 17 12:32:35 2026 -0700

    PHOENIX-7998 Phase 2: Distance functions and operators
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 phoenix-core-client/pom.xml                        |  49 ++
 phoenix-core-client/src/main/antlr3/PhoenixSQL.g   |  25 +-
 .../apache/phoenix/exception/SQLExceptionCode.java |   1 +
 .../apache/phoenix/expression/ExpressionType.java  |   6 +-
 .../function/CosineDistanceFunction.java           |  73 +++
 .../expression/function/DistanceFunction.java      | 173 +++++++
 .../expression/function/DistanceKernel.java        |  46 ++
 .../function/InnerProductDistanceFunction.java     |  64 +++
 .../expression/function/L2DistanceFunction.java    |  64 +++
 .../function/L2DistanceSquaredFunction.java        |  72 +++
 .../expression/function/ScalarDistanceKernel.java  | 120 +++++
 .../expression/function/VectorDistanceUtil.java    |  77 +++
 .../phoenix/parse/DistanceFunctionParseNode.java   |  43 ++
 .../org/apache/phoenix/parse/ParseNodeFactory.java |  15 +
 .../expression/function/DistanceKernel.java        |  88 ++++
 .../expression/function/PanamaDistanceKernel.java  | 122 +++++
 phoenix-core/pom.xml                               |  35 ++
 .../expression/function/DistanceFunctionsTest.java | 556 +++++++++++++++++++++
 .../function/VectorDistanceUtilTest.java           | 135 +++++
 .../phoenix/parse/DistanceOperatorParseTest.java   | 536 ++++++++++++++++++++
 .../function/PanamaDistanceKernelParityTest.java   | 108 ++++
 pom.xml                                            |   8 +
 22 files changed, 2414 insertions(+), 2 deletions(-)

diff --git a/phoenix-core-client/pom.xml b/phoenix-core-client/pom.xml
index 095b5dd5c7..c745b59702 100644
--- a/phoenix-core-client/pom.xml
+++ b/phoenix-core-client/pom.xml
@@ -447,4 +447,53 @@
       </plugin>
     </plugins>
   </build>
+
+  <profiles>
+    <profile>
+      <id>java21</id>
+      <!-- Multi-release build profile compiling SIMD kernels with Panama 
Vector API. -->
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>compile-java21</id>
+                <goals>
+                  <goal>compile</goal>
+                </goals>
+                <phase>compile</phase>
+                <configuration>
+                  <release>21</release>
+                  <compileSourceRoots>
+                    
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
+                  </compileSourceRoots>
+                  <multiReleaseOutput>true</multiReleaseOutput>
+                  <compilerArgs>
+                    <arg>--add-modules</arg>
+                    <arg>jdk.incubator.vector</arg>
+                  </compilerArgs>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-jar-plugin</artifactId>
+            <configuration>
+              <archive>
+                <manifestEntries>
+                  <Multi-Release>true</Multi-Release>
+                </manifestEntries>
+              </archive>
+              <excludes>
+                <exclude>META-INF/versions/*/META-INF/**</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g 
b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
index af0f0e5d9d..369d27c5d8 100644
--- a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
+++ b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
@@ -169,6 +169,10 @@ tokens
     CONSISTENCY = 'consistency';
     EVENTUAL = 'eventual';
     STRONG = 'strong';
+    // Infix vector distance operator tokens (<->, <=>, <#>)
+    DIST_L2;
+    DIST_COSINE;
+    DIST_INNER;
 }
 
 
@@ -1196,7 +1200,20 @@ bind_expression  returns [BindParseNode ret]
     ;
     
 value_expression returns [ParseNode ret]
-    :   i=add_expression { $ret = i; }
+    :   i=distance_expression { $ret = i; }
+    ;
+
+distance_expression returns [ParseNode ret]
+@init{ParseNode lhs = null; List<ParseNode> l;}
+    :   i=add_expression {lhs = i;}
+        (op=(DIST_L2 | DIST_COSINE | DIST_INNER) rhs=add_expression {
+            l = Arrays.asList(lhs, rhs);
+            lhs = op.getType() == DIST_L2 ? factory.l2Distance(l)
+                : op.getType() == DIST_COSINE ? factory.cosineDistance(l)
+                : factory.innerProductDistance(l);
+            }
+        )*
+        { $ret = lhs; }
     ;
 
 add_expression returns [ParseNode ret]
@@ -1514,8 +1531,14 @@ EQ
     :   '='
     ;
 
+// Disambiguate infix distance operators (<=>, <->, <#>) from less-than 
comparisons
 LT
     :   '<'
+        (   ('=' '>') => '=' '>'  { $type = DIST_COSINE; }
+        |   ('-' '>') => '-' '>'  { $type = DIST_L2; }
+        |   ('#' '>') => '#' '>'  { $type = DIST_INNER; }
+        |                         { $type = LT; }
+        )
     ;
 
 GT
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/exception/SQLExceptionCode.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/exception/SQLExceptionCode.java
index c4b33a7649..3611dbf1df 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/exception/SQLExceptionCode.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/exception/SQLExceptionCode.java
@@ -89,6 +89,7 @@ public enum SQLExceptionCode {
   MISSING_MAX_LENGTH(207, "22004", "Max length must be specified for type."),
   NONPOSITIVE_MAX_LENGTH(208, "22006", "Max length must have a positive length 
for type."),
   VECTOR_DIMENSION_EXCEEDED(210, "22003", "Vector dimension exceeds the 
configured maximum."),
+  VECTOR_DIMENSION_MISMATCH(211, "22000", "Vector dimensions do not match."),
   DECIMAL_PRECISION_OUT_OF_RANGE(209, "22003",
     "Decimal precision outside of range. Should be within 1 and " + 
PDataType.MAX_PRECISION + "."),
   SERVER_ARITHMETIC_ERROR(212, "22012", "Arithmetic error on server."),
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/ExpressionType.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/ExpressionType.java
index 6b280cf9de..c857a6f2e6 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/ExpressionType.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/ExpressionType.java
@@ -211,7 +211,11 @@ public enum ExpressionType {
   RawRowSizeFunction(RawRowSizeFunction.class),
   RegexpLikeFunction(RegexpLikeFunction.class),
   ByteBasedRegexpLikeFunction(ByteBasedRegexpLikeFunction.class),
-  StringBasedRegexpLikeFunction(StringBasedRegexpLikeFunction.class);
+  StringBasedRegexpLikeFunction(StringBasedRegexpLikeFunction.class),
+  L2DistanceFunction(L2DistanceFunction.class),
+  L2DistanceSquaredFunction(L2DistanceSquaredFunction.class),
+  CosineDistanceFunction(CosineDistanceFunction.class),
+  InnerProductDistanceFunction(InnerProductDistanceFunction.class);
 
   ExpressionType(Class<? extends Expression> clazz) {
     this.clazz = clazz;
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/CosineDistanceFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/CosineDistanceFunction.java
new file mode 100644
index 0000000000..8e7fb8a04d
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/CosineDistanceFunction.java
@@ -0,0 +1,73 @@
+/*
+ * 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.phoenix.expression.function;
+
+import java.util.List;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.parse.DistanceFunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode.Argument;
+import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+
+/**
+ * Cosine distance metric: {@code 1 - dot(a, b) / (norm(a) * norm(b))}. Zero 
magnitude vectors yield
+ * an orthogonal distance of 1.0.
+ */
+@BuiltInFunction(name = CosineDistanceFunction.NAME, nodeClass = 
DistanceFunctionParseNode.class,
+    args = { @Argument(allowedTypes = { PVectorFloat.class, 
PVectorDouble.class }),
+      @Argument(allowedTypes = { PVectorFloat.class, PVectorDouble.class }) })
+public class CosineDistanceFunction extends DistanceFunction {
+
+  public static final String NAME = "COSINE_DISTANCE";
+
+  public CosineDistanceFunction() {
+  }
+
+  public CosineDistanceFunction(List<Expression> children) {
+    super(children);
+  }
+
+  @Override
+  public String getName() {
+    return NAME;
+  }
+
+  @Override
+  protected double computeFloat(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    return VectorDistanceUtil.cosineDistanceWithBound(a, aOff, b, bOff, dim, 
bound);
+  }
+
+  @Override
+  protected double computeGeneric(byte[] a, int aOff, boolean aIsDouble, 
SortOrder aOrder, byte[] b,
+    int bOff, boolean bIsDouble, SortOrder bOrder, int dim, double bound) {
+    double dot = 0.0;
+    double normA = 0.0;
+    double normB = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double ai = element(a, aOff, aIsDouble, aOrder, i);
+      double bi = element(b, bOff, bIsDouble, bOrder, i);
+      dot += ai * bi;
+      normA += ai * ai;
+      normB += bi * bi;
+    }
+    return 
VectorDistanceUtil.applyBound(ScalarDistanceKernel.cosineDistance(dot, normA, 
normB),
+      bound);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceFunction.java
new file mode 100644
index 0000000000..8dc4008cd8
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceFunction.java
@@ -0,0 +1,173 @@
+/*
+ * 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.phoenix.expression.function;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.exception.SQLExceptionCode;
+import org.apache.phoenix.exception.SQLExceptionInfo;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.schema.IllegalDataException;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.tuple.Tuple;
+import org.apache.phoenix.schema.types.PDataType;
+import org.apache.phoenix.schema.types.PDouble;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+
+/**
+ * Base class for vector distance functions. Evaluates distances directly over 
packed byte encodings
+ * without deserializing vector elements to the heap. Each evaluated result 
allocates a new encoded
+ * buffer to maintain pointer validity across downstream sort pipelines.
+ */
+public abstract class DistanceFunction extends ScalarFunction {
+
+  /**
+   * Transient upper bound for early termination during bounded top-N scans. 
Candidates exceeding
+   * this bound return {@link Double#MAX_VALUE} without completing distance 
evaluation. Not
+   * serialized across RPC boundaries.
+   */
+  private double distanceUpperBound = Double.MAX_VALUE;
+
+  public DistanceFunction() {
+  }
+
+  public DistanceFunction(List<Expression> children) {
+    super(children);
+  }
+
+  /**
+   * Validates vector type compatibility and dimension matching when compile 
time dimensions are
+   * available. Dynamic dimensions are deferred to runtime evaluation.
+   */
+  public static void validateChildren(List<Expression> children) throws 
SQLException {
+    for (int i = 0; i < 2; i++) {
+      PDataType type = children.get(i).getDataType();
+      if (type != null && !type.isVectorType()) {
+        throw new SQLExceptionInfo.Builder(SQLExceptionCode.TYPE_MISMATCH)
+          .setMessage("Distance function argument " + (i + 1) + " must be a 
vector, but was "
+            + type.getSqlTypeName())
+          .build().buildException();
+      }
+    }
+    Integer dim1 = children.get(0).getMaxLength();
+    Integer dim2 = children.get(1).getMaxLength();
+    if (dim1 != null && dim2 != null && !dim1.equals(dim2)) {
+      throw dimensionMismatch(dim1, dim2);
+    }
+  }
+
+  private static SQLException dimensionMismatch(int dim1, int dim2) {
+    return new 
SQLExceptionInfo.Builder(SQLExceptionCode.VECTOR_DIMENSION_MISMATCH)
+      .setMessage(dim1 + " != " + dim2).build().buildException();
+  }
+
+  public double getDistanceUpperBound() {
+    return distanceUpperBound;
+  }
+
+  public void setDistanceUpperBound(double distanceUpperBound) {
+    this.distanceUpperBound = distanceUpperBound;
+  }
+
+  @Override
+  public PDataType getDataType() {
+    return PDouble.INSTANCE;
+  }
+
+  @Override
+  public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
+    Expression child1 = children.get(0);
+    if (!child1.evaluate(tuple, ptr)) {
+      return false;
+    }
+    if (ptr.getLength() == 0) {
+      return true;
+    }
+    byte[] buf1 = ptr.get();
+    int off1 = ptr.getOffset();
+    boolean isDouble1 = isDouble(child1);
+    int dim1 = ptr.getLength() / (isDouble1 ? Bytes.SIZEOF_DOUBLE : 
Bytes.SIZEOF_FLOAT);
+
+    // Retain first operand buffer reference prior to evaluating second child
+    Expression child2 = children.get(1);
+    if (!child2.evaluate(tuple, ptr)) {
+      return false;
+    }
+    if (ptr.getLength() == 0) {
+      return true;
+    }
+    byte[] buf2 = ptr.get();
+    int off2 = ptr.getOffset();
+    boolean isDouble2 = isDouble(child2);
+    int dim2 = ptr.getLength() / (isDouble2 ? Bytes.SIZEOF_DOUBLE : 
Bytes.SIZEOF_FLOAT);
+    if (dim1 != dim2) {
+      throw new IllegalDataException(dimensionMismatch(dim1, dim2));
+    }
+
+    double distance;
+    if (
+      !isDouble1 && !isDouble2 && child1.getSortOrder() == SortOrder.ASC
+        && child2.getSortOrder() == SortOrder.ASC
+    ) {
+      distance = computeFloat(buf1, off1, buf2, off2, dim1, 
distanceUpperBound);
+    } else {
+      distance = computeGeneric(buf1, off1, isDouble1, child1.getSortOrder(), 
buf2, off2, isDouble2,
+        child2.getSortOrder(), dim1, distanceUpperBound);
+    }
+    byte[] out = new byte[Bytes.SIZEOF_DOUBLE];
+    PDouble.INSTANCE.getCodec().encodeDouble(distance, out, 0);
+    ptr.set(out);
+    return true;
+  }
+
+  private static boolean isDouble(Expression child) {
+    return child.getDataType() == PVectorDouble.INSTANCE;
+  }
+
+  /**
+   * Evaluates distance between ASC-ordered float vectors via {@link 
VectorDistanceUtil} kernels.
+   */
+  protected abstract double computeFloat(byte[] a, int aOff, byte[] b, int 
bOff, int dim,
+    double bound);
+
+  /** Evaluates distance across mixed element precision and sort orders. */
+  protected abstract double computeGeneric(byte[] a, int aOff, boolean 
aIsDouble, SortOrder aOrder,
+    byte[] b, int bOff, boolean bIsDouble, SortOrder bOrder, int dim, double 
bound);
+
+  /** Decodes a vector element at the specified index for given precision and 
sort order. */
+  protected static double element(byte[] buf, int off, boolean isDouble, 
SortOrder order, int i) {
+    return isDouble
+      ? PVectorDouble.readElement(buf, off, i, order)
+      : PVectorFloat.readElement(buf, off, i, order);
+  }
+
+  @Override
+  public DistanceFunction clone(List<Expression> children) {
+    try {
+      DistanceFunction clone =
+        (DistanceFunction) 
getClass().getConstructor(List.class).newInstance(children);
+      clone.setDistanceUpperBound(this.distanceUpperBound);
+      return clone;
+    } catch (ReflectiveOperationException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceKernel.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceKernel.java
new file mode 100644
index 0000000000..950e54a3e9
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/DistanceKernel.java
@@ -0,0 +1,46 @@
+/*
+ * 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.phoenix.expression.function;
+
+/**
+ * Baseline distance kernels for query time vector distance evaluation over 
packed float vectors.
+ * Delegates to {@link ScalarDistanceKernel}. A multi-release Java 21 variant 
overlays this class to
+ * dispatch to Panama Vector API implementations when available.
+ */
+final class DistanceKernel {
+
+  private DistanceKernel() {
+  }
+
+  /** Indicates whether query time distances are computed using SIMD kernels. 
*/
+  static boolean isSimd() {
+    return false;
+  }
+
+  static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    return ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, 
bound);
+  }
+
+  static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) {
+    return ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim);
+  }
+
+  static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int 
dim) {
+    return ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/InnerProductDistanceFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/InnerProductDistanceFunction.java
new file mode 100644
index 0000000000..cde17e0b4e
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/InnerProductDistanceFunction.java
@@ -0,0 +1,64 @@
+/*
+ * 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.phoenix.expression.function;
+
+import java.util.List;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.parse.DistanceFunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode.Argument;
+import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+
+/** Negative inner product distance metric: {@code -dot(a, b)}. */
+@BuiltInFunction(name = InnerProductDistanceFunction.NAME,
+    nodeClass = DistanceFunctionParseNode.class,
+    args = { @Argument(allowedTypes = { PVectorFloat.class, 
PVectorDouble.class }),
+      @Argument(allowedTypes = { PVectorFloat.class, PVectorDouble.class }) })
+public class InnerProductDistanceFunction extends DistanceFunction {
+
+  public static final String NAME = "INNER_PRODUCT";
+
+  public InnerProductDistanceFunction() {
+  }
+
+  public InnerProductDistanceFunction(List<Expression> children) {
+    super(children);
+  }
+
+  @Override
+  public String getName() {
+    return NAME;
+  }
+
+  @Override
+  protected double computeFloat(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    return VectorDistanceUtil.innerProductDistanceWithBound(a, aOff, b, bOff, 
dim, bound);
+  }
+
+  @Override
+  protected double computeGeneric(byte[] a, int aOff, boolean aIsDouble, 
SortOrder aOrder, byte[] b,
+    int bOff, boolean bIsDouble, SortOrder bOrder, int dim, double bound) {
+    double dot = 0.0;
+    for (int i = 0; i < dim; i++) {
+      dot += element(a, aOff, aIsDouble, aOrder, i) * element(b, bOff, 
bIsDouble, bOrder, i);
+    }
+    return VectorDistanceUtil.applyBound(-dot, bound);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceFunction.java
new file mode 100644
index 0000000000..82e3cce35f
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceFunction.java
@@ -0,0 +1,64 @@
+/*
+ * 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.phoenix.expression.function;
+
+import java.util.List;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.parse.DistanceFunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode.Argument;
+import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+
+/** Euclidean distance metric: {@code sqrt(sum((a[i] - b[i])^2))}. */
+@BuiltInFunction(name = L2DistanceFunction.NAME, nodeClass = 
DistanceFunctionParseNode.class,
+    args = { @Argument(allowedTypes = { PVectorFloat.class, 
PVectorDouble.class }),
+      @Argument(allowedTypes = { PVectorFloat.class, PVectorDouble.class }) })
+public class L2DistanceFunction extends DistanceFunction {
+
+  public static final String NAME = "L2_DISTANCE";
+
+  public L2DistanceFunction() {
+  }
+
+  public L2DistanceFunction(List<Expression> children) {
+    super(children);
+  }
+
+  @Override
+  public String getName() {
+    return NAME;
+  }
+
+  @Override
+  protected double computeFloat(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    return VectorDistanceUtil.l2DistanceWithBound(a, aOff, b, bOff, dim, 
bound);
+  }
+
+  @Override
+  protected double computeGeneric(byte[] a, int aOff, boolean aIsDouble, 
SortOrder aOrder, byte[] b,
+    int bOff, boolean bIsDouble, SortOrder bOrder, int dim, double bound) {
+    double boundSq = bound == Double.MAX_VALUE ? Double.MAX_VALUE : bound * 
bound;
+    double sq = L2DistanceSquaredFunction.squared(a, aOff, aIsDouble, aOrder, 
b, bOff, bIsDouble,
+      bOrder, dim, boundSq);
+    return sq == Double.MAX_VALUE
+      ? Double.MAX_VALUE
+      : VectorDistanceUtil.applyBound(Math.sqrt(sq), bound);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceSquaredFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceSquaredFunction.java
new file mode 100644
index 0000000000..b7bdab408a
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/L2DistanceSquaredFunction.java
@@ -0,0 +1,72 @@
+/*
+ * 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.phoenix.expression.function;
+
+import java.util.List;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.parse.DistanceFunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode.Argument;
+import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+
+/** Squared Euclidean distance metric: {@code sum((a[i] - b[i])^2)}. */
+@BuiltInFunction(name = L2DistanceSquaredFunction.NAME, nodeClass = 
DistanceFunctionParseNode.class,
+    args = { @Argument(allowedTypes = { PVectorFloat.class, 
PVectorDouble.class }),
+      @Argument(allowedTypes = { PVectorFloat.class, PVectorDouble.class }) })
+public class L2DistanceSquaredFunction extends DistanceFunction {
+
+  public static final String NAME = "L2_DISTANCE_SQUARED";
+
+  public L2DistanceSquaredFunction() {
+  }
+
+  public L2DistanceSquaredFunction(List<Expression> children) {
+    super(children);
+  }
+
+  @Override
+  public String getName() {
+    return NAME;
+  }
+
+  @Override
+  protected double computeFloat(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    return VectorDistanceUtil.l2DistanceSquaredWithBound(a, aOff, b, bOff, 
dim, bound);
+  }
+
+  @Override
+  protected double computeGeneric(byte[] a, int aOff, boolean aIsDouble, 
SortOrder aOrder, byte[] b,
+    int bOff, boolean bIsDouble, SortOrder bOrder, int dim, double bound) {
+    return squared(a, aOff, aIsDouble, aOrder, b, bOff, bIsDouble, bOrder, 
dim, bound);
+  }
+
+  static double squared(byte[] a, int aOff, boolean aIsDouble, SortOrder 
aOrder, byte[] b, int bOff,
+    boolean bIsDouble, SortOrder bOrder, int dim, double bound) {
+    double sum = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double diff = element(a, aOff, aIsDouble, aOrder, i) - element(b, bOff, 
bIsDouble, bOrder, i);
+      sum += diff * diff;
+      if (sum > bound) {
+        return Double.MAX_VALUE;
+      }
+    }
+    return sum;
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
new file mode 100644
index 0000000000..7d014c1a28
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
@@ -0,0 +1,120 @@
+/*
+ * 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.phoenix.expression.function;
+
+import org.apache.hadoop.hbase.util.Bytes;
+
+/**
+ * Reference scalar distance kernels over packed float byte buffers and {@code 
float[]} arrays.
+ * Accumulates intermediate calculations in double precision to provide 
consistent reference
+ * baselines across execution platforms.
+ */
+final class ScalarDistanceKernel {
+
+  private ScalarDistanceKernel() {
+  }
+
+  private static float read(byte[] buf, int off, int i) {
+    return Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT);
+  }
+
+  /**
+   * Evaluates squared Euclidean distance with early termination when the 
partial sum exceeds the
+   * specified bound.
+   */
+  static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    double sum = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double diff = read(a, aOff, i) - read(b, bOff, i);
+      sum += diff * diff;
+      if (sum > bound) {
+        return Double.MAX_VALUE;
+      }
+    }
+    return sum;
+  }
+
+  static double l2DistanceSquared(float[] a, float[] b, int dim, double bound) 
{
+    double sum = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double diff = a[i] - b[i];
+      sum += diff * diff;
+      if (sum > bound) {
+        return Double.MAX_VALUE;
+      }
+    }
+    return sum;
+  }
+
+  static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) {
+    double dot = 0.0;
+    for (int i = 0; i < dim; i++) {
+      dot += (double) read(a, aOff, i) * read(b, bOff, i);
+    }
+    return dot;
+  }
+
+  static double dotProduct(float[] a, float[] b, int dim) {
+    double dot = 0.0;
+    for (int i = 0; i < dim; i++) {
+      dot += (double) a[i] * b[i];
+    }
+    return dot;
+  }
+
+  static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int 
dim) {
+    double dot = 0.0;
+    double normA = 0.0;
+    double normB = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double ai = read(a, aOff, i);
+      double bi = read(b, bOff, i);
+      dot += ai * bi;
+      normA += ai * ai;
+      normB += bi * bi;
+    }
+    return cosineDistance(dot, normA, normB);
+  }
+
+  static double cosineDistance(float[] a, float[] b, int dim) {
+    double dot = 0.0;
+    double normA = 0.0;
+    double normB = 0.0;
+    for (int i = 0; i < dim; i++) {
+      double ai = a[i];
+      double bi = b[i];
+      dot += ai * bi;
+      normA += ai * ai;
+      normB += bi * bi;
+    }
+    return cosineDistance(dot, normA, normB);
+  }
+
+  /**
+   * Computes cosine distance from pre-accumulated dot product and norms. Zero 
magnitude operands
+   * default to an orthogonal distance of 1.0.
+   */
+  static double cosineDistance(double dot, double normA, double normB) {
+    double denom = Math.sqrt(normA) * Math.sqrt(normB);
+    if (denom == 0.0) {
+      return 1.0;
+    }
+    double similarity = Math.max(-1.0, Math.min(1.0, dot / denom));
+    return 1.0 - similarity;
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/VectorDistanceUtil.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/VectorDistanceUtil.java
new file mode 100644
index 0000000000..085bf72109
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/VectorDistanceUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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.phoenix.expression.function;
+
+/**
+ * Utility methods for vector distance computations over packed byte buffers 
and array structures.
+ * <p>
+ * Bounded query time methods delegate to {@link DistanceKernel}, supporting 
SIMD acceleration and
+ * early termination when candidate distances exceed the provided threshold. 
Pass
+ * {@link Double#MAX_VALUE} for unbounded evaluation.
+ * <p>
+ * Unbounded scalar methods bypass SIMD execution paths to guarantee 
deterministic floating point
+ * results across platforms during centroid training and assignment.
+ */
+public final class VectorDistanceUtil {
+
+  private VectorDistanceUtil() {
+  }
+
+  /** Indicates whether SIMD distance kernels are active in the current JVM 
runtime. */
+  public static boolean isSimdEnabled() {
+    return DistanceKernel.isSimd();
+  }
+
+  public static double scalarL2DistanceSquared(float[] a, float[] b, int dim) {
+    return ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE);
+  }
+
+  public static double scalarInnerProductDistance(float[] a, float[] b, int 
dim) {
+    return -ScalarDistanceKernel.dotProduct(a, b, dim);
+  }
+
+  public static double scalarCosineDistance(float[] a, float[] b, int dim) {
+    return ScalarDistanceKernel.cosineDistance(a, b, dim);
+  }
+
+  public static double l2DistanceSquaredWithBound(byte[] a, int aOff, byte[] 
b, int bOff, int dim,
+    double bound) {
+    return DistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound);
+  }
+
+  public static double l2DistanceWithBound(byte[] a, int aOff, byte[] b, int 
bOff, int dim,
+    double bound) {
+    double boundSq = bound == Double.MAX_VALUE ? Double.MAX_VALUE : bound * 
bound;
+    double sq = DistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, 
boundSq);
+    return sq == Double.MAX_VALUE ? Double.MAX_VALUE : 
applyBound(Math.sqrt(sq), bound);
+  }
+
+  public static double innerProductDistanceWithBound(byte[] a, int aOff, 
byte[] b, int bOff,
+    int dim, double bound) {
+    return applyBound(-DistanceKernel.dotProduct(a, aOff, b, bOff, dim), 
bound);
+  }
+
+  public static double cosineDistanceWithBound(byte[] a, int aOff, byte[] b, 
int bOff, int dim,
+    double bound) {
+    return applyBound(DistanceKernel.cosineDistance(a, aOff, b, bOff, dim), 
bound);
+  }
+
+  static double applyBound(double distance, double bound) {
+    return distance > bound ? Double.MAX_VALUE : distance;
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DistanceFunctionParseNode.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DistanceFunctionParseNode.java
new file mode 100644
index 0000000000..72b3378bd9
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DistanceFunctionParseNode.java
@@ -0,0 +1,43 @@
+/*
+ * 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.phoenix.parse;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.apache.phoenix.compile.StatementContext;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.expression.function.DistanceFunction;
+
+/**
+ * Parse node for vector distance functions. Validates vector argument 
compatibility and dimension
+ * consistency prior to constructing the function expression.
+ */
+public class DistanceFunctionParseNode extends FunctionParseNode {
+
+  public DistanceFunctionParseNode(String name, List<ParseNode> children,
+    BuiltInFunctionInfo info) {
+    super(name, children, info);
+  }
+
+  @Override
+  public Expression create(List<Expression> children, StatementContext context)
+    throws SQLException {
+    DistanceFunction.validateChildren(children);
+    return super.create(children, context);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
index 2a36dc4035..da08d77a76 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
@@ -33,11 +33,14 @@ import org.apache.hadoop.hbase.util.Pair;
 import org.apache.phoenix.expression.Expression;
 import org.apache.phoenix.expression.ExpressionType;
 import org.apache.phoenix.expression.function.AvgAggregateFunction;
+import org.apache.phoenix.expression.function.CosineDistanceFunction;
 import org.apache.phoenix.expression.function.CountAggregateFunction;
 import org.apache.phoenix.expression.function.CurrentDateFunction;
 import org.apache.phoenix.expression.function.CurrentTimeFunction;
 import org.apache.phoenix.expression.function.DistinctCountAggregateFunction;
 import org.apache.phoenix.expression.function.FunctionExpression;
+import org.apache.phoenix.expression.function.InnerProductDistanceFunction;
+import org.apache.phoenix.expression.function.L2DistanceFunction;
 import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunction;
 import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunctionInfo;
 import org.apache.phoenix.parse.JoinTableNode.JoinType;
@@ -243,6 +246,18 @@ public class ParseNodeFactory {
     return new ModulusParseNode(children);
   }
 
+  public FunctionParseNode l2Distance(List<ParseNode> children) {
+    return function(L2DistanceFunction.NAME, children);
+  }
+
+  public FunctionParseNode cosineDistance(List<ParseNode> children) {
+    return function(CosineDistanceFunction.NAME, children);
+  }
+
+  public FunctionParseNode innerProductDistance(List<ParseNode> children) {
+    return function(InnerProductDistanceFunction.NAME, children);
+  }
+
   public AndParseNode and(List<ParseNode> children) {
     return new AndParseNode(children);
   }
diff --git 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
new file mode 100644
index 0000000000..6fff9f6cb8
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
@@ -0,0 +1,88 @@
+/*
+ * 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.phoenix.expression.function;
+
+import org.apache.hadoop.hbase.util.Bytes;
+
+/**
+ * Java 21 multi-release implementation of {@code DistanceKernel}. Dispatches 
to
+ * {@link PanamaDistanceKernel} when {@code jdk.incubator.vector} is 
available, falling back to
+ * {@link ScalarDistanceKernel}.
+ * <p>
+ * Decodes packed big-endian float vectors into thread local float buffers 
prior to SIMD execution
+ * to satisfy Vector API buffer alignment and endianness requirements.
+ */
+final class DistanceKernel {
+
+  private static final boolean SIMD = isPanamaAvailable();
+
+  private static final ThreadLocal<float[][]> SCRATCH =
+    ThreadLocal.withInitial(() -> new float[][] { new float[0], new float[0] 
});
+
+  private DistanceKernel() {
+  }
+
+  static boolean isSimd() {
+    return SIMD;
+  }
+
+  private static boolean isPanamaAvailable() {
+    try {
+      Class.forName("jdk.incubator.vector.FloatVector");
+      return PanamaDistanceKernel.dotProduct(new float[] { 1f }, new float[] { 
1f }, 1) == 1.0;
+    } catch (Throwable t) {
+      return false;
+    }
+  }
+
+  private static float[] decode(int slot, byte[] buf, int off, int dim) {
+    float[][] scratch = SCRATCH.get();
+    float[] out = scratch[slot];
+    if (out.length < dim) {
+      out = new float[dim];
+      scratch[slot] = out;
+    }
+    for (int i = 0; i < dim; i++) {
+      out[i] = Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT);
+    }
+    return out;
+  }
+
+  static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
+    if (!SIMD) {
+      return ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, 
bound);
+    }
+    return PanamaDistanceKernel.l2DistanceSquared(decode(0, a, aOff, dim), 
decode(1, b, bOff, dim),
+      dim, bound);
+  }
+
+  static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) {
+    if (!SIMD) {
+      return ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim);
+    }
+    return PanamaDistanceKernel.dotProduct(decode(0, a, aOff, dim), decode(1, 
b, bOff, dim), dim);
+  }
+
+  static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int 
dim) {
+    if (!SIMD) {
+      return ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim);
+    }
+    return PanamaDistanceKernel.cosineDistance(decode(0, a, aOff, dim), 
decode(1, b, bOff, dim),
+      dim);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
new file mode 100644
index 0000000000..386f1a4f86
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
@@ -0,0 +1,122 @@
+/*
+ * 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.phoenix.expression.function;
+
+import jdk.incubator.vector.FloatVector;
+import jdk.incubator.vector.VectorOperators;
+import jdk.incubator.vector.VectorSpecies;
+
+/**
+ * SIMD distance kernels leveraging the Panama Vector API over {@code float[]} 
operands.
+ * <p>
+ * Accumulates intermediate lane sums in single precision across fixed size 
blocks of
+ * {@link #BLOCK_STRIDES} vector strides before reducing to double precision. 
Block level reduction
+ * bounds floating point accumulation drift relative to {@link 
ScalarDistanceKernel} while enabling
+ * periodic upper bound pruning without per-stride reduction overhead.
+ */
+final class PanamaDistanceKernel {
+
+  private static final VectorSpecies<Float> SPECIES = 
FloatVector.SPECIES_PREFERRED;
+  static final int BLOCK_STRIDES = 8;
+
+  private PanamaDistanceKernel() {
+  }
+
+  static double l2DistanceSquared(float[] a, float[] b, int dim, double bound) 
{
+    int step = SPECIES.length();
+    int upper = SPECIES.loopBound(dim);
+    int blockLen = step * BLOCK_STRIDES;
+    double total = 0.0;
+    int i = 0;
+    while (i < upper) {
+      int blockEnd = Math.min(upper, i + blockLen);
+      FloatVector acc = FloatVector.zero(SPECIES);
+      for (; i < blockEnd; i += step) {
+        FloatVector diff =
+          FloatVector.fromArray(SPECIES, a, 
i).sub(FloatVector.fromArray(SPECIES, b, i));
+        acc = diff.fma(diff, acc);
+      }
+      total += acc.reduceLanes(VectorOperators.ADD);
+      if (total > bound) {
+        return Double.MAX_VALUE;
+      }
+    }
+    for (; i < dim; i++) {
+      double diff = a[i] - b[i];
+      total += diff * diff;
+      if (total > bound) {
+        return Double.MAX_VALUE;
+      }
+    }
+    return total;
+  }
+
+  static double dotProduct(float[] a, float[] b, int dim) {
+    int step = SPECIES.length();
+    int upper = SPECIES.loopBound(dim);
+    int blockLen = step * BLOCK_STRIDES;
+    double total = 0.0;
+    int i = 0;
+    while (i < upper) {
+      int blockEnd = Math.min(upper, i + blockLen);
+      FloatVector acc = FloatVector.zero(SPECIES);
+      for (; i < blockEnd; i += step) {
+        acc = FloatVector.fromArray(SPECIES, a, 
i).fma(FloatVector.fromArray(SPECIES, b, i), acc);
+      }
+      total += acc.reduceLanes(VectorOperators.ADD);
+    }
+    for (; i < dim; i++) {
+      total += (double) a[i] * b[i];
+    }
+    return total;
+  }
+
+  static double cosineDistance(float[] a, float[] b, int dim) {
+    int step = SPECIES.length();
+    int upper = SPECIES.loopBound(dim);
+    int blockLen = step * BLOCK_STRIDES;
+    double dot = 0.0;
+    double normA = 0.0;
+    double normB = 0.0;
+    int i = 0;
+    while (i < upper) {
+      int blockEnd = Math.min(upper, i + blockLen);
+      FloatVector vDot = FloatVector.zero(SPECIES);
+      FloatVector vNormA = FloatVector.zero(SPECIES);
+      FloatVector vNormB = FloatVector.zero(SPECIES);
+      for (; i < blockEnd; i += step) {
+        FloatVector va = FloatVector.fromArray(SPECIES, a, i);
+        FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
+        vDot = va.fma(vb, vDot);
+        vNormA = va.fma(va, vNormA);
+        vNormB = vb.fma(vb, vNormB);
+      }
+      dot += vDot.reduceLanes(VectorOperators.ADD);
+      normA += vNormA.reduceLanes(VectorOperators.ADD);
+      normB += vNormB.reduceLanes(VectorOperators.ADD);
+    }
+    for (; i < dim; i++) {
+      double ai = a[i];
+      double bi = b[i];
+      dot += ai * bi;
+      normA += ai * ai;
+      normB += bi * bi;
+    }
+    return ScalarDistanceKernel.cosineDistance(dot, normA, normB);
+  }
+}
diff --git a/phoenix-core/pom.xml b/phoenix-core/pom.xml
index a69db4b597..a488f171b1 100644
--- a/phoenix-core/pom.xml
+++ b/phoenix-core/pom.xml
@@ -518,4 +518,39 @@
       </plugin>
     </plugins>
   </build>
+  <profiles>
+    <profile>
+      <!-- Compiles Java 21 multi-release distance kernels and tests to 
execute test suite
+           with SIMD acceleration. -->
+      <id>java21</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>test-compile-java21</id>
+                <goals>
+                  <goal>testCompile</goal>
+                </goals>
+                <phase>test-compile</phase>
+                <configuration>
+                  <release>21</release>
+                  <compileSourceRoots>
+                    
<compileSourceRoot>${project.basedir}/../phoenix-core-client/src/main/java21</compileSourceRoot>
+                    
<compileSourceRoot>${project.basedir}/src/test/java21</compileSourceRoot>
+                  </compileSourceRoots>
+                  <compilerArgs>
+                    <arg>--add-modules</arg>
+                    <arg>jdk.incubator.vector</arg>
+                  </compilerArgs>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/DistanceFunctionsTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/DistanceFunctionsTest.java
new file mode 100644
index 0000000000..1332dcab39
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/DistanceFunctionsTest.java
@@ -0,0 +1,556 @@
+/*
+ * 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.phoenix.expression.function;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.io.WritableUtils;
+import org.apache.phoenix.compile.QueryPlan;
+import org.apache.phoenix.exception.SQLExceptionCode;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.expression.ExpressionType;
+import org.apache.phoenix.expression.LiteralExpression;
+import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
+import org.apache.phoenix.parse.ColumnParseNode;
+import org.apache.phoenix.parse.DistanceFunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode;
+import org.apache.phoenix.parse.FunctionParseNode.BuiltInFunctionInfo;
+import org.apache.phoenix.parse.ParseNode;
+import org.apache.phoenix.parse.ParseNodeFactory;
+import org.apache.phoenix.parse.SQLParser;
+import org.apache.phoenix.parse.SelectStatement;
+import org.apache.phoenix.query.BaseConnectionlessQueryTest;
+import org.apache.phoenix.schema.IllegalDataException;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PDouble;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+import org.junit.Test;
+
+import org.apache.phoenix.thirdparty.com.google.common.collect.Multimap;
+
+public class DistanceFunctionsTest extends BaseConnectionlessQueryTest {
+
+  private static final double DELTA = 1e-6;
+
+  private static <T extends DistanceFunction> T withBound(T function, double 
bound) {
+    function.setDistanceUpperBound(bound);
+    return function;
+  }
+
+  private static BuiltInFunctionInfo lookup(String name) {
+    Collection<BuiltInFunctionInfo> infos = 
ParseNodeFactory.getBuiltInFunctionMultimap().get(name);
+    return infos.isEmpty() ? null : infos.iterator().next();
+  }
+
+  private static void assertDistanceNode(String name, ParseNode node) {
+    assertTrue("Expected DistanceFunctionParseNode, got " + 
node.getClass().getSimpleName(),
+      node instanceof DistanceFunctionParseNode);
+    assertEquals(name, ((FunctionParseNode) node).getName());
+  }
+
+  private static double evaluateDistance(DistanceFunction function) {
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable();
+    boolean evaluated = function.evaluate(null, ptr);
+    assertTrue("Distance function should evaluate successfully", evaluated);
+    assertNotNull("Pointer should not be null", ptr.get());
+    Object obj = PDouble.INSTANCE.toObject(ptr);
+    assertNotNull("Decoded distance should not be null", obj);
+    return ((Number) obj).doubleValue();
+  }
+
+  @Test
+  public void testL2DistanceCorrectness() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE);
+    L2DistanceFunction func = new L2DistanceFunction(Arrays.asList(v1, v2));
+
+    double dist = evaluateDistance(func);
+    assertEquals(5.0, dist, DELTA);
+  }
+
+  @Test
+  public void testL2DistanceSquaredCorrectness() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE);
+    L2DistanceSquaredFunction func = new 
L2DistanceSquaredFunction(Arrays.asList(v1, v2));
+
+    double dist = evaluateDistance(func);
+    assertEquals(25.0, dist, DELTA);
+  }
+
+  @Test
+  public void testCosineDistanceOrthogonalVectors() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 1.0f }, 
PVectorFloat.INSTANCE);
+    CosineDistanceFunction func = new CosineDistanceFunction(Arrays.asList(v1, 
v2));
+
+    double dist = evaluateDistance(func);
+    assertEquals(1.0, dist, DELTA);
+  }
+
+  @Test
+  public void testCosineDistanceIdenticalAndOpposite() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    CosineDistanceFunction funcIdentical = new 
CosineDistanceFunction(Arrays.asList(v1, v2));
+    assertEquals(0.0, evaluateDistance(funcIdentical), DELTA);
+
+    Expression vOpposite =
+      LiteralExpression.newConstant(new float[] { -1.0f, -2.0f, -3.0f }, 
PVectorFloat.INSTANCE);
+    CosineDistanceFunction funcOpposite = new 
CosineDistanceFunction(Arrays.asList(v1, vOpposite));
+    assertEquals(2.0, evaluateDistance(funcOpposite), DELTA);
+  }
+
+  @Test
+  public void testCosineDistanceZeroVector() throws Exception {
+    Expression vZero =
+      LiteralExpression.newConstant(new float[] { 0.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    CosineDistanceFunction func = new 
CosineDistanceFunction(Arrays.asList(vZero, v1));
+    assertEquals(1.0, evaluateDistance(func), DELTA);
+  }
+
+  @Test
+  public void testInnerProductDistance() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 4.0f, 5.0f }, 
PVectorFloat.INSTANCE);
+    InnerProductDistanceFunction func = new 
InnerProductDistanceFunction(Arrays.asList(v1, v2));
+
+    double dist = evaluateDistance(func);
+    assertEquals(-23.0, dist, DELTA);
+  }
+
+  @Test
+  public void testVectorDoubleSupport() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new double[] { 3.0, 0.0 }, 
PVectorDouble.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new double[] { 0.0, 4.0 }, 
PVectorDouble.INSTANCE);
+
+    L2DistanceFunction l2 = new L2DistanceFunction(Arrays.asList(v1, v2));
+    assertEquals(5.0, evaluateDistance(l2), DELTA);
+
+    L2DistanceSquaredFunction l2Sq = new 
L2DistanceSquaredFunction(Arrays.asList(v1, v2));
+    assertEquals(25.0, evaluateDistance(l2Sq), DELTA);
+
+    Expression d1 =
+      LiteralExpression.newConstant(new double[] { 2.0, 3.0 }, 
PVectorDouble.INSTANCE);
+    Expression d2 =
+      LiteralExpression.newConstant(new double[] { 4.0, 5.0 }, 
PVectorDouble.INSTANCE);
+    InnerProductDistanceFunction ip = new 
InnerProductDistanceFunction(Arrays.asList(d1, d2));
+    assertEquals(-23.0, evaluateDistance(ip), DELTA);
+
+    Expression o1 =
+      LiteralExpression.newConstant(new double[] { 1.0, 0.0 }, 
PVectorDouble.INSTANCE);
+    Expression o2 =
+      LiteralExpression.newConstant(new double[] { 0.0, 1.0 }, 
PVectorDouble.INSTANCE);
+    CosineDistanceFunction cosine = new 
CosineDistanceFunction(Arrays.asList(o1, o2));
+    assertEquals(1.0, evaluateDistance(cosine), DELTA);
+  }
+
+  @Test
+  public void testDescendingSortOrder() throws Exception {
+    Expression v1 = LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE,
+      SortOrder.DESC);
+    Expression v2 = LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE,
+      SortOrder.DESC);
+
+    L2DistanceFunction l2 = new L2DistanceFunction(Arrays.asList(v1, v2));
+    assertEquals(5.0, evaluateDistance(l2), DELTA);
+
+    Expression v3 = LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE,
+      SortOrder.ASC);
+    L2DistanceFunction l2Mixed = new L2DistanceFunction(Arrays.asList(v1, v3));
+    assertEquals(5.0, evaluateDistance(l2Mixed), DELTA);
+  }
+
+  @Test
+  public void testDimensionMismatchCompileTime() throws Exception {
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f }, 
PVectorFloat.INSTANCE);
+    Expression v3 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    try {
+      DistanceFunction.validateChildren(Arrays.asList(v2, v3));
+      fail("Expected SQLException for dimension mismatch");
+    } catch (SQLException e) {
+      assertEquals(SQLExceptionCode.VECTOR_DIMENSION_MISMATCH.getErrorCode(), 
e.getErrorCode());
+    }
+  }
+
+  @Test
+  public void testDimensionMismatchAtEvaluation() throws Exception {
+    // Runtime dimension mismatch validation during row evaluation
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f }, 
PVectorFloat.INSTANCE);
+    Expression v3 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    for (DistanceFunction func : new DistanceFunction[] {
+      new L2DistanceFunction(Arrays.asList(v2, v3)),
+      new L2DistanceSquaredFunction(Arrays.asList(v2, v3)),
+      new CosineDistanceFunction(Arrays.asList(v2, v3)),
+      new InnerProductDistanceFunction(Arrays.asList(v2, v3)) }) {
+      try {
+        func.evaluate(null, new ImmutableBytesWritable());
+        fail("Expected IllegalDataException for " + func.getName());
+      } catch (IllegalDataException e) {
+        assertEquals(SQLExceptionCode.VECTOR_DIMENSION_MISMATCH.getErrorCode(),
+          ((SQLException) e.getCause()).getErrorCode());
+      }
+    }
+  }
+
+  @Test
+  public void testNullPropagation() throws Exception {
+    Expression nullVec = LiteralExpression.newConstant(null, 
PVectorFloat.INSTANCE);
+    Expression validVec =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f }, 
PVectorFloat.INSTANCE);
+
+    DistanceFunction[] funcs =
+      new DistanceFunction[] { new L2DistanceFunction(Arrays.asList(nullVec, 
validVec)),
+        new L2DistanceFunction(Arrays.asList(validVec, nullVec)),
+        new L2DistanceFunction(Arrays.asList(nullVec, nullVec)),
+        new L2DistanceSquaredFunction(Arrays.asList(nullVec, validVec)),
+        new CosineDistanceFunction(Arrays.asList(nullVec, validVec)),
+        new InnerProductDistanceFunction(Arrays.asList(nullVec, validVec)) };
+
+    for (DistanceFunction func : funcs) {
+      ImmutableBytesWritable ptr = new ImmutableBytesWritable(new byte[] { 1 
});
+      assertTrue("A null operand is evaluable for " + func.getName(), 
func.evaluate(null, ptr));
+      assertEquals("Expected SQL NULL for " + func.getName(), 0, 
ptr.getLength());
+    }
+  }
+
+  @Test
+  public void testEarlyTerminationUpperBound() throws Exception {
+    Expression v1 =
+      LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE);
+
+    // Verify squared L2 distance upper bound pruning
+    L2DistanceSquaredFunction l2SqExceeded =
+      withBound(new L2DistanceSquaredFunction(Arrays.asList(v1, v2)), 10.0);
+    assertEquals(Double.MAX_VALUE, evaluateDistance(l2SqExceeded), DELTA);
+
+    L2DistanceSquaredFunction l2SqMet =
+      withBound(new L2DistanceSquaredFunction(Arrays.asList(v1, v2)), 30.0);
+    assertEquals(25.0, evaluateDistance(l2SqMet), DELTA);
+
+    // Verify L2 distance upper bound pruning
+    L2DistanceFunction l2Exceeded = withBound(new 
L2DistanceFunction(Arrays.asList(v1, v2)), 4.0);
+    assertEquals(Double.MAX_VALUE, evaluateDistance(l2Exceeded), DELTA);
+
+    L2DistanceFunction l2Met = withBound(new 
L2DistanceFunction(Arrays.asList(v1, v2)), 6.0);
+    assertEquals(5.0, evaluateDistance(l2Met), DELTA);
+  }
+
+  @Test
+  public void testParseNodeCreation() throws Exception {
+    Expression v2 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f }, 
PVectorFloat.INSTANCE);
+    Expression v3 =
+      LiteralExpression.newConstant(new float[] { 1.0f, 2.0f, 3.0f }, 
PVectorFloat.INSTANCE);
+    Expression v2b =
+      LiteralExpression.newConstant(new float[] { 3.0f, 4.0f }, 
PVectorFloat.INSTANCE);
+    List<ParseNode> dummyNodes = Collections.emptyList();
+    for (int i = 0; i < DISTANCE_FUNCTION_NAMES.length; i++) {
+      String name = DISTANCE_FUNCTION_NAMES[i];
+      DistanceFunctionParseNode node =
+        new DistanceFunctionParseNode(name, dummyNodes, lookup(name));
+      try {
+        node.create(Arrays.asList(v2, v3), null);
+        fail("Expected SQLException on dimension mismatch in parse node for " 
+ name);
+      } catch (SQLException e) {
+        
assertEquals(SQLExceptionCode.VECTOR_DIMENSION_MISMATCH.getErrorCode(), 
e.getErrorCode());
+      }
+      assertTrue(
+        DISTANCE_FUNCTION_CLASSES[i].isInstance(node.create(Arrays.asList(v2, 
v2b), null)));
+    }
+  }
+
+  @Test
+  public void testExpressionTypeVIntSerializationRoundTrip() throws Exception {
+    // Verify ExpressionType resolution and VInt serialization round-trip 
across distance functions
+    List<Class<? extends Expression>> functionClasses =
+      Arrays.asList(L2DistanceFunction.class, L2DistanceSquaredFunction.class,
+        CosineDistanceFunction.class, InnerProductDistanceFunction.class);
+
+    for (Class<? extends Expression> clazz : functionClasses) {
+      Expression expression = clazz.getDeclaredConstructor().newInstance();
+      ExpressionType expectedType = ExpressionType.valueOf(expression);
+      assertEquals(clazz, expectedType.getExpressionClass());
+
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      DataOutputStream out = new DataOutputStream(baos);
+      WritableUtils.writeVInt(out, expectedType.ordinal());
+      out.close();
+
+      DataInputStream in = new DataInputStream(new 
ByteArrayInputStream(baos.toByteArray()));
+      int decoded = WritableUtils.readVInt(in);
+      Expression resolved = ExpressionType.values()[decoded].newInstance();
+      assertTrue(clazz.getName() + " should round-trip via VInt ordinal",
+        clazz.isInstance(resolved));
+    }
+  }
+
+  @Test
+  public void testExpressionTypeFullSerializationRoundTrip() throws Exception {
+    // Full expression serialization round-trip against orthogonal test vectors
+    LiteralExpression child1 =
+      LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE, 2, null);
+    LiteralExpression child2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE, 2, null);
+
+    double[] expectedValues = { 5.0, 25.0, 1.0, 0.0 };
+    List<DistanceFunction> functions =
+      Arrays.asList(new L2DistanceFunction(Arrays.asList(child1, child2)),
+        new L2DistanceSquaredFunction(Arrays.asList(child1, child2)),
+        new CosineDistanceFunction(Arrays.asList(child1, child2)),
+        new InnerProductDistanceFunction(Arrays.asList(child1, child2)));
+
+    for (int i = 0; i < functions.size(); i++) {
+      DistanceFunction func = functions.get(i);
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      DataOutputStream out = new DataOutputStream(baos);
+      WritableUtils.writeVInt(out, ExpressionType.valueOf(func).ordinal());
+      func.write(out);
+      out.close();
+
+      DataInputStream in = new DataInputStream(new 
ByteArrayInputStream(baos.toByteArray()));
+      int ordinal = WritableUtils.readVInt(in);
+      Expression deserialized = ExpressionType.values()[ordinal].newInstance();
+      deserialized.readFields(in);
+
+      assertTrue(func.getClass().isInstance(deserialized));
+      DistanceFunction deserializedDistance = (DistanceFunction) deserialized;
+      assertEquals(Double.MAX_VALUE, 
deserializedDistance.getDistanceUpperBound(), 0.0);
+      assertEquals(2, deserializedDistance.getChildren().size());
+
+      ImmutableBytesWritable ptr = new ImmutableBytesWritable();
+      assertTrue(deserializedDistance.evaluate(null, ptr));
+      double val = (Double) PDouble.INSTANCE.toObject(ptr);
+      assertEquals(func.getClass().getSimpleName() + " returned unexpected 
distance",
+        expectedValues[i], val, DELTA);
+    }
+  }
+
+  private static final String[] DISTANCE_FUNCTION_NAMES =
+    { L2DistanceFunction.NAME, L2DistanceSquaredFunction.NAME, 
CosineDistanceFunction.NAME,
+      InnerProductDistanceFunction.NAME };
+
+  private static final Class<?>[] DISTANCE_FUNCTION_CLASSES =
+    { L2DistanceFunction.class, L2DistanceSquaredFunction.class, 
CosineDistanceFunction.class,
+      InnerProductDistanceFunction.class };
+
+  @Test
+  public void testFunctionLookupByName() {
+    for (int i = 0; i < DISTANCE_FUNCTION_NAMES.length; i++) {
+      String name = DISTANCE_FUNCTION_NAMES[i];
+      BuiltInFunctionInfo info = lookup(name);
+      assertNotNull("Function lookup failed for: " + name, info);
+      assertEquals(name, info.getName());
+      assertEquals(2, info.getRequiredArgCount());
+      assertEquals(2, info.getArgs().length);
+      assertEquals(DistanceFunctionParseNode.class, 
info.getNodeCtor().getDeclaringClass());
+      assertEquals(DISTANCE_FUNCTION_CLASSES[i], info.getFunc());
+
+      // Verify accepted argument types for vector distance functions
+      for (FunctionParseNode.BuiltInFunctionArgInfo arg : info.getArgs()) {
+        List<Class<?>> allowedTypes = Arrays.asList(arg.getAllowedTypes());
+        assertTrue(name + " arg should allow PVectorFloat",
+          allowedTypes.contains(PVectorFloat.class));
+        assertTrue(name + " arg should allow PVectorDouble",
+          allowedTypes.contains(PVectorDouble.class));
+      }
+    }
+  }
+
+  @Test
+  public void testFunctionLookupWithChildrenList() {
+    ParseNode c1 = new ColumnParseNode(null, "V1", null);
+    ParseNode c2 = new ColumnParseNode(null, "V2", null);
+    List<ParseNode> children2 = Arrays.asList(c1, c2);
+    List<ParseNode> children1 = Arrays.asList(c1);
+
+    for (String name : DISTANCE_FUNCTION_NAMES) {
+      BuiltInFunctionInfo info = ParseNodeFactory.get(name, children2);
+      assertNotNull("Lookup with 2 children failed for: " + name, info);
+      assertEquals(name, info.getName());
+
+      assertNull("Lookup with 1 child should be null for: " + name,
+        ParseNodeFactory.get(name, children1));
+    }
+  }
+
+  @Test
+  public void testFunctionUniquenessAndNoCollisions() {
+    Multimap<String, BuiltInFunctionInfo> multimap = 
ParseNodeFactory.getBuiltInFunctionMultimap();
+
+    for (String name : DISTANCE_FUNCTION_NAMES) {
+      assertTrue("Multimap should contain: " + name, 
multimap.containsKey(name));
+      Collection<BuiltInFunctionInfo> infos = multimap.get(name);
+      assertEquals("Expected exactly 1 registered function for " + name, 1, 
infos.size());
+      BuiltInFunctionInfo info = infos.iterator().next();
+      assertEquals(name, info.getName());
+    }
+
+    // Validate lookup failure for unregistered function names
+    assertFalse(multimap.containsKey("NON_EXISTENT_DISTANCE_FUNC"));
+  }
+
+  @Test
+  public void testParseNodeFactoryDirectMethods() {
+    ParseNodeFactory factory = new ParseNodeFactory();
+    ParseNode c1 = new ColumnParseNode(null, "V1", null);
+    ParseNode c2 = new ColumnParseNode(null, "V2", null);
+    List<ParseNode> children = Arrays.asList(c1, c2);
+
+    assertDistanceNode(L2DistanceFunction.NAME, factory.l2Distance(children));
+    assertDistanceNode(CosineDistanceFunction.NAME, 
factory.cosineDistance(children));
+    assertDistanceNode(InnerProductDistanceFunction.NAME, 
factory.innerProductDistance(children));
+    for (String name : DISTANCE_FUNCTION_NAMES) {
+      assertDistanceNode(name, factory.function(name, children));
+    }
+  }
+
+  @Test
+  public void testSQLParsingDistanceFunctions() throws Exception {
+    for (String name : DISTANCE_FUNCTION_NAMES) {
+      for (String spelling : new String[] { name, name.toLowerCase() }) {
+        SelectStatement select =
+          (SelectStatement) new SQLParser("SELECT " + spelling + "(v1, v2) 
FROM t")
+            .parseStatement();
+        ParseNode node = select.getSelect().get(0).getNode();
+        assertDistanceNode(name, node);
+        assertEquals(2, node.getChildren().size());
+      }
+    }
+  }
+
+  @Test
+  public void testEndToEndSQLCompilation() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute(
+        "CREATE TABLE t_vector_func_test (" + "pk INTEGER PRIMARY KEY, " + "v1 
VECTOR(FLOAT, 3), "
+          + "v2 VECTOR(FLOAT, 3), " + "vd1 VECTOR(DOUBLE, 4), " + "vd2 
VECTOR(DOUBLE, 4))");
+
+      String[] queries = { "SELECT L2_DISTANCE(v1, v2) FROM 
t_vector_func_test",
+        "SELECT L2_DISTANCE_SQUARED(v1, v2) FROM t_vector_func_test",
+        "SELECT COSINE_DISTANCE(v1, v2) FROM t_vector_func_test",
+        "SELECT INNER_PRODUCT(v1, v2) FROM t_vector_func_test",
+        "SELECT L2_DISTANCE(vd1, vd2) FROM t_vector_func_test",
+        "SELECT L2_DISTANCE_SQUARED(vd1, vd2) FROM t_vector_func_test",
+        "SELECT COSINE_DISTANCE(vd1, vd2) FROM t_vector_func_test",
+        "SELECT INNER_PRODUCT(vd1, vd2) FROM t_vector_func_test",
+        "SELECT pk FROM t_vector_func_test WHERE L2_DISTANCE(v1, v2) < 2.5",
+        "SELECT pk FROM t_vector_func_test ORDER BY COSINE_DISTANCE(v1, v2) 
LIMIT 10",
+        "SELECT L2_DISTANCE(v1, v2) + 1.0 AS dist FROM t_vector_func_test" };
+
+      for (String sql : queries) {
+        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+          PhoenixPreparedStatement pStmt = 
stmt.unwrap(PhoenixPreparedStatement.class);
+          QueryPlan plan = pStmt.compileQuery();
+          assertNotNull("Plan should not be null for query: " + sql, plan);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testCompilationFailsOnDimensionMismatch() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute("CREATE TABLE t_dim_mismatch (" + "pk 
INTEGER PRIMARY KEY, "
+        + "v3 VECTOR(FLOAT, 3), " + "v4 VECTOR(FLOAT, 4))");
+
+      try (PreparedStatement stmt =
+        conn.prepareStatement("SELECT L2_DISTANCE(v3, v4) FROM 
t_dim_mismatch")) {
+        PhoenixPreparedStatement pStmt = 
stmt.unwrap(PhoenixPreparedStatement.class);
+        pStmt.compileQuery();
+        fail("Expected compilation to fail due to dimension mismatch");
+      } catch (SQLException e) {
+        
assertEquals(SQLExceptionCode.VECTOR_DIMENSION_MISMATCH.getErrorCode(), 
e.getErrorCode());
+      }
+    }
+  }
+
+  @Test
+  public void testCompilationFailsOnNonVectorArgument() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute(
+        "CREATE TABLE t_non_vector (" + "pk INTEGER PRIMARY KEY, " + "v 
VECTOR(FLOAT, 3))");
+
+      try (PreparedStatement stmt =
+        conn.prepareStatement("SELECT L2_DISTANCE(pk, v) FROM t_non_vector")) {
+        PhoenixPreparedStatement pStmt = 
stmt.unwrap(PhoenixPreparedStatement.class);
+        pStmt.compileQuery();
+        fail("Expected compilation to fail due to non-vector argument");
+      } catch (SQLException e) {
+        assertEquals(SQLExceptionCode.TYPE_MISMATCH.getErrorCode(), 
e.getErrorCode());
+      }
+    }
+  }
+
+  @Test
+  public void testCompilationFailsOnUnknownFunction() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute("CREATE TABLE t_unknown_func (" + "pk 
INTEGER PRIMARY KEY, "
+        + "v1 VECTOR(FLOAT, 3), " + "v2 VECTOR(FLOAT, 3))");
+
+      try (PreparedStatement stmt =
+        conn.prepareStatement("SELECT UNKNOWN_DISTANCE_FUNC(v1, v2) FROM 
t_unknown_func")) {
+        PhoenixPreparedStatement pStmt = 
stmt.unwrap(PhoenixPreparedStatement.class);
+        pStmt.compileQuery();
+        fail("Expected compilation to fail for unknown function");
+      } catch (SQLException e) {
+        // Function resolution failure for unregistered functions
+        assertNotNull(e.getMessage());
+      }
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/VectorDistanceUtilTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/VectorDistanceUtilTest.java
new file mode 100644
index 0000000000..895fddb8ea
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/VectorDistanceUtilTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.phoenix.expression.function;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Random;
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.expression.LiteralExpression;
+import org.apache.phoenix.schema.types.PDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+import org.junit.Test;
+
+public class VectorDistanceUtilTest {
+
+  private static final double DELTA = 1e-5;
+
+  /**
+   * Validates distance upper bound pruning and boundary conditions.
+   */
+  @Test
+  public void testEarlyTerminationCorrectness() {
+    byte[] b1 = PVectorFloat.INSTANCE.toBytes(new float[] { 3.0f, 0.0f });
+    byte[] b2 = PVectorFloat.INSTANCE.toBytes(new float[] { 0.0f, 4.0f });
+
+    assertEquals(Double.MAX_VALUE, VectorDistanceUtil.l2DistanceWithBound(b1, 
0, b2, 0, 2, 1.0),
+      0.0);
+    assertEquals(5.0, VectorDistanceUtil.l2DistanceWithBound(b1, 0, b2, 0, 2, 
10.0), DELTA);
+
+    byte[] sqB1 = PVectorFloat.INSTANCE.toBytes(new float[] { 1.0f, 2.0f });
+    byte[] sqB2 = PVectorFloat.INSTANCE.toBytes(new float[] { 0.0f, 0.0f });
+    assertEquals(Double.MAX_VALUE,
+      VectorDistanceUtil.l2DistanceSquaredWithBound(sqB1, 0, sqB2, 0, 2, 1.0), 
0.0);
+    assertEquals(5.0, VectorDistanceUtil.l2DistanceSquaredWithBound(sqB1, 0, 
sqB2, 0, 2, 10.0),
+      DELTA);
+
+    assertEquals(Double.MAX_VALUE,
+      VectorDistanceUtil.l2DistanceSquaredWithBound(b1, 0, b2, 0, 2, 1.0), 
0.0);
+    assertEquals(25.0, VectorDistanceUtil.l2DistanceSquaredWithBound(b1, 0, 
b2, 0, 2, 30.0), DELTA);
+
+    // Inner product and cosine evaluate across full vectors before bound 
filtering
+    assertEquals(Double.MAX_VALUE,
+      VectorDistanceUtil.innerProductDistanceWithBound(b1, 0, b2, 0, 2, -1.0), 
0.0);
+    assertEquals(0.0,
+      VectorDistanceUtil.innerProductDistanceWithBound(b1, 0, b2, 0, 2, 
Double.MAX_VALUE), DELTA);
+    assertEquals(Double.MAX_VALUE, 
VectorDistanceUtil.cosineDistanceWithBound(b1, 0, b2, 0, 2, 0.5),
+      0.0);
+    assertEquals(1.0, VectorDistanceUtil.cosineDistanceWithBound(b1, 0, b2, 0, 
2, 1.5), DELTA);
+  }
+
+  /**
+   * Validates early termination in bounded scalar L2 kernels using truncated 
operand buffers.
+   */
+  @Test
+  public void testEarlyTerminationStopsReading() {
+    int dim = 128;
+    int readable = dim / 4;
+    float[] v1 = new float[readable];
+    Arrays.fill(v1, (float) (5.0 / Math.sqrt(dim)));
+    float[] v2 = new float[readable];
+    // Running sum exceeds threshold within truncated buffer window
+    assertEquals(Double.MAX_VALUE, ScalarDistanceKernel.l2DistanceSquared(v1, 
v2, dim, 1.0), 0.0);
+    byte[] b1 = PVectorFloat.INSTANCE.toBytes(v1);
+    byte[] b2 = PVectorFloat.INSTANCE.toBytes(v2);
+    assertEquals(Double.MAX_VALUE, ScalarDistanceKernel.l2DistanceSquared(b1, 
0, b2, 0, dim, 1.0),
+      0.0);
+  }
+
+  /**
+   * Validates calculation consistency between scalar reference kernels and 
query-time entry points.
+   */
+  @Test
+  public void testScalarAndPackedAgree() {
+    Random rng = new Random(3);
+    for (int dim : new int[] { 1, 5, 64, 300 }) {
+      float[] a = new float[dim];
+      float[] b = new float[dim];
+      for (int i = 0; i < dim; i++) {
+        a[i] = (rng.nextFloat() - 0.5f) * 20.0f;
+        b[i] = (rng.nextFloat() - 0.5f) * 20.0f;
+      }
+      byte[] pa = PVectorFloat.INSTANCE.toBytes(a);
+      byte[] pb = PVectorFloat.INSTANCE.toBytes(b);
+      assertClose(VectorDistanceUtil.scalarL2DistanceSquared(a, b, dim),
+        VectorDistanceUtil.l2DistanceSquaredWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+      assertClose(VectorDistanceUtil.scalarInnerProductDistance(a, b, dim),
+        VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+      assertClose(VectorDistanceUtil.scalarCosineDistance(a, b, dim),
+        VectorDistanceUtil.cosineDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+    }
+  }
+
+  private static void assertClose(double expected, double actual) {
+    assertEquals(expected, actual, DELTA * Math.max(1.0, Math.abs(expected)));
+  }
+
+  /**
+   * Validates bounded distance function evaluation during top-N scan 
execution.
+   */
+  @Test
+  public void testDistanceFunctionEvaluateWithBound() throws Exception {
+    Expression child1 =
+      LiteralExpression.newConstant(new float[] { 3.0f, 0.0f }, 
PVectorFloat.INSTANCE, 2, null);
+    Expression child2 =
+      LiteralExpression.newConstant(new float[] { 0.0f, 4.0f }, 
PVectorFloat.INSTANCE, 2, null);
+    L2DistanceFunction func = new L2DistanceFunction(Arrays.asList(child1, 
child2));
+
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable();
+    func.setDistanceUpperBound(10.0);
+    assertTrue(func.evaluate(null, ptr));
+    assertEquals(5.0, ((Number) PDouble.INSTANCE.toObject(ptr)).doubleValue(), 
DELTA);
+
+    func.setDistanceUpperBound(1.0);
+    assertTrue(func.evaluate(null, ptr));
+    assertEquals(Double.MAX_VALUE, ((Number) 
PDouble.INSTANCE.toObject(ptr)).doubleValue(), 0.0);
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/parse/DistanceOperatorParseTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/DistanceOperatorParseTest.java
new file mode 100644
index 0000000000..d5ec49e007
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/DistanceOperatorParseTest.java
@@ -0,0 +1,536 @@
+/*
+ * 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.phoenix.parse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import org.apache.phoenix.util.QueryUtil;
+import org.junit.Test;
+
+/**
+ * Parser tests validating AST generation and operator precedence for infix 
vector distance
+ * operators (<->, <=>, <#>).
+ */
+public class DistanceOperatorParseTest {
+
+  private static boolean isDistance(ParseNode node, String name) {
+    return node instanceof DistanceFunctionParseNode
+      && name.equals(((FunctionParseNode) node).getName());
+  }
+
+  @Test
+  public void testParseL2DistanceOperator() throws Exception {
+    String sql = "SELECT v1 <-> v2 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    assertEquals(1, select.getSelect().size());
+    ParseNode node = select.getSelect().get(0).getNode();
+    assertTrue("Expected L2_DISTANCE node, got: " + 
node.getClass().getSimpleName(),
+      isDistance(node, "L2_DISTANCE"));
+    assertEquals(2, node.getChildren().size());
+    assertTrue(node.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue(node.getChildren().get(1) instanceof ColumnParseNode);
+    assertEquals("V1", ((ColumnParseNode) 
node.getChildren().get(0)).getName());
+    assertEquals("V2", ((ColumnParseNode) 
node.getChildren().get(1)).getName());
+  }
+
+  @Test
+  public void testParseCosineDistanceOperator() throws Exception {
+    String sql = "SELECT v1 <=> v2 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    assertEquals(1, select.getSelect().size());
+    ParseNode node = select.getSelect().get(0).getNode();
+    assertTrue("Expected COSINE_DISTANCE node, got: " + 
node.getClass().getSimpleName(),
+      isDistance(node, "COSINE_DISTANCE"));
+    assertEquals(2, node.getChildren().size());
+    assertTrue(node.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue(node.getChildren().get(1) instanceof ColumnParseNode);
+    assertEquals("V1", ((ColumnParseNode) 
node.getChildren().get(0)).getName());
+    assertEquals("V2", ((ColumnParseNode) 
node.getChildren().get(1)).getName());
+  }
+
+  @Test
+  public void testParseInnerProductDistanceOperator() throws Exception {
+    String sql = "SELECT v1 <#> v2 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    assertEquals(1, select.getSelect().size());
+    ParseNode node = select.getSelect().get(0).getNode();
+    assertTrue("Expected INNER_PRODUCT node, got: " + 
node.getClass().getSimpleName(),
+      isDistance(node, "INNER_PRODUCT"));
+    assertEquals(2, node.getChildren().size());
+    assertTrue(node.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue(node.getChildren().get(1) instanceof ColumnParseNode);
+    assertEquals("V1", ((ColumnParseNode) 
node.getChildren().get(0)).getName());
+    assertEquals("V2", ((ColumnParseNode) 
node.getChildren().get(1)).getName());
+  }
+
+  @Test
+  public void testOperatorFunctionEquivalence() throws Exception {
+    // L2 distance operator equivalence
+    SelectStatement opL2 =
+      (SelectStatement) new SQLParser("SELECT v1 <-> v2 FROM 
t").parseStatement();
+    SelectStatement fnL2 =
+      (SelectStatement) new SQLParser("SELECT L2_DISTANCE(v1, v2) FROM 
t").parseStatement();
+    ParseNode opL2Node = opL2.getSelect().get(0).getNode();
+    ParseNode fnL2Node = fnL2.getSelect().get(0).getNode();
+    assertTrue(isDistance(opL2Node, "L2_DISTANCE"));
+    assertEquals(fnL2Node.getClass(), opL2Node.getClass());
+    assertEquals(fnL2Node, opL2Node);
+
+    // Cosine distance operator equivalence
+    SelectStatement opCos =
+      (SelectStatement) new SQLParser("SELECT v1 <=> v2 FROM 
t").parseStatement();
+    SelectStatement fnCos =
+      (SelectStatement) new SQLParser("SELECT COSINE_DISTANCE(v1, v2) FROM 
t").parseStatement();
+    ParseNode opCosNode = opCos.getSelect().get(0).getNode();
+    ParseNode fnCosNode = fnCos.getSelect().get(0).getNode();
+    assertTrue(isDistance(opCosNode, "COSINE_DISTANCE"));
+    assertEquals(fnCosNode.getClass(), opCosNode.getClass());
+    assertEquals(fnCosNode, opCosNode);
+
+    // Inner product operator equivalence
+    SelectStatement opIp =
+      (SelectStatement) new SQLParser("SELECT v1 <#> v2 FROM 
t").parseStatement();
+    SelectStatement fnIp =
+      (SelectStatement) new SQLParser("SELECT INNER_PRODUCT(v1, v2) FROM 
t").parseStatement();
+    ParseNode opIpNode = opIp.getSelect().get(0).getNode();
+    ParseNode fnIpNode = fnIp.getSelect().get(0).getNode();
+    assertTrue(isDistance(opIpNode, "INNER_PRODUCT"));
+    assertEquals(fnIpNode.getClass(), opIpNode.getClass());
+    assertEquals(fnIpNode, opIpNode);
+  }
+
+  @Test
+  public void testPrecedenceDistanceVsComparison() throws Exception {
+    // Distance operators bind with higher precedence than relational 
comparisons
+    String sql = "SELECT v1 <-> v2 < 0.5 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode root = select.getSelect().get(0).getNode();
+    assertTrue("Expected LessThanParseNode, got: " + 
root.getClass().getSimpleName(),
+      root instanceof LessThanParseNode);
+    assertEquals(2, root.getChildren().size());
+    ParseNode lhs = root.getChildren().get(0);
+    ParseNode rhs = root.getChildren().get(1);
+    assertTrue("LHS should be L2_DISTANCE node, got: " + 
lhs.getClass().getSimpleName(),
+      isDistance(lhs, "L2_DISTANCE"));
+    assertTrue("RHS should be LiteralParseNode, got: " + 
rhs.getClass().getSimpleName(),
+      rhs instanceof LiteralParseNode);
+    assertEquals(2, lhs.getChildren().size());
+    assertEquals("V1", ((ColumnParseNode) lhs.getChildren().get(0)).getName());
+    assertEquals("V2", ((ColumnParseNode) lhs.getChildren().get(1)).getName());
+  }
+
+  @Test
+  public void testPrecedenceArithmeticVsDistance() throws Exception {
+    // Arithmetic operators bind with higher precedence than distance operators
+    String sql = "SELECT v1 + v2 <-> v3 * v4 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode root = select.getSelect().get(0).getNode();
+    assertTrue("Expected L2_DISTANCE node, got: " + 
root.getClass().getSimpleName(),
+      isDistance(root, "L2_DISTANCE"));
+    assertEquals(2, root.getChildren().size());
+    ParseNode lhs = root.getChildren().get(0);
+    ParseNode rhs = root.getChildren().get(1);
+    assertTrue("LHS should be AddParseNode, got: " + 
lhs.getClass().getSimpleName(),
+      lhs instanceof AddParseNode);
+    assertTrue("RHS should be MultiplyParseNode, got: " + 
rhs.getClass().getSimpleName(),
+      rhs instanceof MultiplyParseNode);
+  }
+
+  @Test
+  public void testParenthesesOverridePrecedence() throws Exception {
+    // Explicit parentheses override default operator precedence
+    String sql = "SELECT (v1 <-> v2) + 1.0 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode root = select.getSelect().get(0).getNode();
+    assertTrue("Expected AddParseNode, got: " + 
root.getClass().getSimpleName(),
+      root instanceof AddParseNode);
+    assertEquals(2, root.getChildren().size());
+    ParseNode lhs = root.getChildren().get(0);
+    assertTrue("LHS should be L2_DISTANCE node, got: " + 
lhs.getClass().getSimpleName(),
+      isDistance(lhs, "L2_DISTANCE"));
+  }
+
+  @Test
+  public void testWhereClauseWithDistanceOperator() throws Exception {
+    String sql = "SELECT * FROM t WHERE v1 <-> v2 < 0.5";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode where = select.getWhere();
+    assertNotNull("WHERE clause should not be null", where);
+    assertTrue("WHERE should be LessThanParseNode", where instanceof 
LessThanParseNode);
+    ParseNode lhs = where.getChildren().get(0);
+    assertTrue("LHS of WHERE comparison should be L2_DISTANCE node",
+      isDistance(lhs, "L2_DISTANCE"));
+  }
+
+  @Test
+  public void testOrderByWithDistanceOperator() throws Exception {
+    String sql = "SELECT doc_id FROM documents ORDER BY embedding <=> :1 LIMIT 
10";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    List<OrderByNode> orderBy = select.getOrderBy();
+    assertNotNull(orderBy);
+    assertEquals(1, orderBy.size());
+    ParseNode orderNode = orderBy.get(0).getNode();
+    assertTrue("ORDER BY node should be COSINE_DISTANCE node",
+      isDistance(orderNode, "COSINE_DISTANCE"));
+    assertEquals(2, orderNode.getChildren().size());
+    assertTrue(orderNode.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue(orderNode.getChildren().get(1) instanceof BindParseNode);
+  }
+
+  @Test
+  public void testNoSpaceDistanceOperator() throws Exception {
+    String sql = "SELECT v1<->v2, v1<=>v2, v1<#>v2 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    assertEquals(3, select.getSelect().size());
+    assertTrue(isDistance(select.getSelect().get(0).getNode(), "L2_DISTANCE"));
+    assertTrue(isDistance(select.getSelect().get(1).getNode(), 
"COSINE_DISTANCE"));
+    assertTrue(isDistance(select.getSelect().get(2).getNode(), 
"INNER_PRODUCT"));
+  }
+
+  @Test
+  public void testComplexLogicalPrecedence() throws Exception {
+    // Verify precedence hierarchy: distance > comparison > NOT > AND > OR
+    String sql =
+      "SELECT * FROM t WHERE v1 <-> v2 < 0.5 AND NOT v3 <=> v4 >= 0.8 OR v1 
<#> v2 < -10.0";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode where = select.getWhere();
+    assertNotNull(where);
+    assertTrue("Root WHERE should be OrParseNode, got: " + 
where.getClass().getSimpleName(),
+      where instanceof OrParseNode);
+    assertEquals(2, where.getChildren().size());
+
+    ParseNode andNode = where.getChildren().get(0);
+    assertTrue("Left of OR should be AndParseNode", andNode instanceof 
AndParseNode);
+    assertEquals(2, andNode.getChildren().size());
+
+    ParseNode leftComp = andNode.getChildren().get(0);
+    assertTrue("Left of AND should be LessThanParseNode", leftComp instanceof 
LessThanParseNode);
+    assertTrue("LHS of < should be L2_DISTANCE node",
+      isDistance(leftComp.getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("RHS of < should be LiteralParseNode",
+      leftComp.getChildren().get(1) instanceof LiteralParseNode);
+
+    ParseNode notNode = andNode.getChildren().get(1);
+    assertTrue("Right of AND should be NotParseNode", notNode instanceof 
NotParseNode);
+    ParseNode notChild = notNode.getChildren().get(0);
+    assertTrue("Child of NOT should be GreaterThanOrEqualParseNode",
+      notChild instanceof GreaterThanOrEqualParseNode);
+    assertTrue("LHS of >= should be COSINE_DISTANCE node",
+      isDistance(notChild.getChildren().get(0), "COSINE_DISTANCE"));
+    assertTrue("RHS of >= should be LiteralParseNode",
+      notChild.getChildren().get(1) instanceof LiteralParseNode);
+
+    ParseNode rightComp = where.getChildren().get(1);
+    assertTrue("Right of OR should be LessThanParseNode", rightComp instanceof 
LessThanParseNode);
+    assertTrue("LHS of < should be INNER_PRODUCT node",
+      isDistance(rightComp.getChildren().get(0), "INNER_PRODUCT"));
+  }
+
+  @Test
+  public void testFullArithmeticSpectrumWithDistance() throws Exception {
+    // Verify arithmetic precedence: multiplicative > additive > distance > 
comparison
+    String sql =
+      "SELECT (v1 + v2 * 2.0) <-> (v3 - v4 / 2.0 % 1.0) < (threshold + 0.1) * 
2.0 FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode root = select.getSelect().get(0).getNode();
+    assertTrue("Expected LessThanParseNode, got: " + 
root.getClass().getSimpleName(),
+      root instanceof LessThanParseNode);
+
+    ParseNode distNode = root.getChildren().get(0);
+    assertTrue("LHS of < should be L2_DISTANCE node", isDistance(distNode, 
"L2_DISTANCE"));
+    assertEquals(2, distNode.getChildren().size());
+
+    ParseNode op1 = distNode.getChildren().get(0);
+    assertTrue("Operand 1 of <-> should be AddParseNode", op1 instanceof 
AddParseNode);
+    assertTrue("Add child 0 should be ColumnParseNode",
+      op1.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue("Add child 1 should be MultiplyParseNode",
+      op1.getChildren().get(1) instanceof MultiplyParseNode);
+
+    ParseNode op2 = distNode.getChildren().get(1);
+    assertTrue("Operand 2 of <-> should be SubtractParseNode", op2 instanceof 
SubtractParseNode);
+    assertTrue("Subtract child 0 should be ColumnParseNode",
+      op2.getChildren().get(0) instanceof ColumnParseNode);
+    ParseNode modNode = op2.getChildren().get(1);
+    assertTrue("Subtract child 1 should be ModulusParseNode", modNode 
instanceof ModulusParseNode);
+    assertTrue("Modulus child 0 should be DivideParseNode",
+      modNode.getChildren().get(0) instanceof DivideParseNode);
+
+    ParseNode rhs = root.getChildren().get(1);
+    assertTrue("RHS of < should be MultiplyParseNode", rhs instanceof 
MultiplyParseNode);
+    assertTrue("Multiply child 0 should be AddParseNode",
+      rhs.getChildren().get(0) instanceof AddParseNode);
+  }
+
+  @Test
+  public void testBetweenClauseWithDistanceExpressions() throws Exception {
+    // Distance expression as BETWEEN target
+    String sql1 = "SELECT * FROM t WHERE v1 <-> v2 BETWEEN 0.1 AND 0.9";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode where1 = select1.getWhere();
+    assertTrue("WHERE should be BetweenParseNode", where1 instanceof 
BetweenParseNode);
+    assertEquals(3, where1.getChildren().size());
+    assertTrue("BETWEEN target should be L2_DISTANCE node",
+      isDistance(where1.getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("BETWEEN lower bound should be LiteralParseNode",
+      where1.getChildren().get(1) instanceof LiteralParseNode);
+    assertTrue("BETWEEN upper bound should be LiteralParseNode",
+      where1.getChildren().get(2) instanceof LiteralParseNode);
+
+    // Distance expressions as BETWEEN range boundaries
+    String sql2 = "SELECT * FROM t WHERE target_dist BETWEEN v1 <-> v2 AND v3 
<=> v4";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    ParseNode where2 = select2.getWhere();
+    assertTrue("WHERE should be BetweenParseNode", where2 instanceof 
BetweenParseNode);
+    assertEquals(3, where2.getChildren().size());
+    assertTrue("BETWEEN target should be ColumnParseNode",
+      where2.getChildren().get(0) instanceof ColumnParseNode);
+    assertTrue("BETWEEN lower bound should be L2_DISTANCE node",
+      isDistance(where2.getChildren().get(1), "L2_DISTANCE"));
+    assertTrue("BETWEEN upper bound should be COSINE_DISTANCE node",
+      isDistance(where2.getChildren().get(2), "COSINE_DISTANCE"));
+  }
+
+  @Test
+  public void testInListWithDistanceExpression() throws Exception {
+    // Distance expression as IN predicate operand
+    String sql = "SELECT * FROM t WHERE v1 <-> v2 IN (0.1, 0.2, 0.3)";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode where = select.getWhere();
+    assertTrue("WHERE should be InListParseNode", where instanceof 
InListParseNode);
+    List<ParseNode> children = where.getChildren();
+    assertEquals(4, children.size());
+    assertTrue("IN target should be L2_DISTANCE node", 
isDistance(children.get(0), "L2_DISTANCE"));
+    assertTrue("IN element 1 should be LiteralParseNode",
+      children.get(1) instanceof LiteralParseNode);
+    assertTrue("IN element 2 should be LiteralParseNode",
+      children.get(2) instanceof LiteralParseNode);
+    assertTrue("IN element 3 should be LiteralParseNode",
+      children.get(3) instanceof LiteralParseNode);
+  }
+
+  @Test
+  public void testCaseWhenWithDistanceExpressions() throws Exception {
+    // Distance expressions within CASE condition and result branches
+    String sql = "SELECT CASE " + "WHEN v1 <=> v2 < 0.2 THEN v1 <-> v2 "
+      + "WHEN v1 <=> v2 < 0.5 THEN (v1 <-> v2) * 1.5 " + "ELSE v1 <#> v2 END 
FROM t";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    ParseNode root = select.getSelect().get(0).getNode();
+    assertTrue("Expected CaseParseNode", root instanceof CaseParseNode);
+    List<ParseNode> children = root.getChildren();
+    assertEquals(5, children.size());
+
+    assertTrue("THEN 1 should be L2_DISTANCE node", 
isDistance(children.get(0), "L2_DISTANCE"));
+    assertTrue("WHEN 1 should be LessThanParseNode", children.get(1) 
instanceof LessThanParseNode);
+    assertTrue("WHEN 1 LHS should be COSINE_DISTANCE node",
+      isDistance(children.get(1).getChildren().get(0), "COSINE_DISTANCE"));
+
+    assertTrue("THEN 2 should be MultiplyParseNode", children.get(2) 
instanceof MultiplyParseNode);
+    assertTrue("THEN 2 child 0 should be L2_DISTANCE node",
+      isDistance(children.get(2).getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("WHEN 2 should be LessThanParseNode", children.get(3) 
instanceof LessThanParseNode);
+    assertTrue("WHEN 2 LHS should be COSINE_DISTANCE node",
+      isDistance(children.get(3).getChildren().get(0), "COSINE_DISTANCE"));
+
+    assertTrue("ELSE should be INNER_PRODUCT node", 
isDistance(children.get(4), "INNER_PRODUCT"));
+  }
+
+  @Test
+  public void testChainedDistanceOperatorsAssociativity() throws Exception {
+    // Left-to-right associativity across chained homogeneous distance 
operators
+    String sql1 = "SELECT v1 <-> v2 <-> v3 FROM t";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode root1 = select1.getSelect().get(0).getNode();
+    assertTrue("Root should be L2_DISTANCE node", isDistance(root1, 
"L2_DISTANCE"));
+    assertEquals(2, root1.getChildren().size());
+    assertTrue("LHS should be L2_DISTANCE node",
+      isDistance(root1.getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("RHS should be ColumnParseNode",
+      root1.getChildren().get(1) instanceof ColumnParseNode);
+    assertEquals("V3", ((ColumnParseNode) 
root1.getChildren().get(1)).getName());
+
+    // Left-to-right associativity across chained heterogeneous distance 
operators
+    String sql2 = "SELECT v1 <-> v2 <=> v3 <#> v4 FROM t";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    ParseNode root2 = select2.getSelect().get(0).getNode();
+    assertTrue("Root should be INNER_PRODUCT node", isDistance(root2, 
"INNER_PRODUCT"));
+    assertEquals(2, root2.getChildren().size());
+
+    ParseNode cosNode = root2.getChildren().get(0);
+    assertTrue("LHS of <#> should be COSINE_DISTANCE node", 
isDistance(cosNode, "COSINE_DISTANCE"));
+    assertTrue("LHS of <=> should be L2_DISTANCE node",
+      isDistance(cosNode.getChildren().get(0), "L2_DISTANCE"));
+    assertEquals("V3", ((ColumnParseNode) 
cosNode.getChildren().get(1)).getName());
+    assertEquals("V4", ((ColumnParseNode) 
root2.getChildren().get(1)).getName());
+  }
+
+  @Test
+  public void testUnaryMinusPrecedenceWithDistance() throws Exception {
+    // Unary negation on vector operand binds with higher precedence than 
distance operator
+    String sql1 = "SELECT -v1 <-> v2 FROM t";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode root1 = select1.getSelect().get(0).getNode();
+    assertTrue("Root should be L2_DISTANCE node", isDistance(root1, 
"L2_DISTANCE"));
+    ParseNode lhs1 = root1.getChildren().get(0);
+    assertTrue("Operand with unary minus should be MultiplyParseNode (negate 
representation)",
+      lhs1 instanceof MultiplyParseNode);
+    assertTrue(root1.getChildren().get(1) instanceof ColumnParseNode);
+
+    // Parentheses apply unary negation to evaluated distance expression
+    String sql2 = "SELECT -(v1 <-> v2) FROM t";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    ParseNode root2 = select2.getSelect().get(0).getNode();
+    assertTrue("Root should be MultiplyParseNode (negate representation)",
+      root2 instanceof MultiplyParseNode);
+    assertTrue("Child of negate should be L2_DISTANCE node",
+      isDistance(root2.getChildren().get(0), "L2_DISTANCE"));
+  }
+
+  @Test
+  public void testNestedFunctionsAndComplexOrderBy() throws Exception {
+    // Distance expressions nested within scalar function calls
+    String sql1 = "SELECT ROUND(v1 <-> v2, 2) > ROUND(v3 <=> v4, 2) FROM t";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode root1 = select1.getSelect().get(0).getNode();
+    assertTrue("Expected GreaterThanParseNode", root1 instanceof 
GreaterThanParseNode);
+    ParseNode leftFn = root1.getChildren().get(0);
+    ParseNode rightFn = root1.getChildren().get(1);
+    assertTrue("LHS should be FunctionParseNode", leftFn instanceof 
FunctionParseNode);
+    assertTrue("RHS should be FunctionParseNode", rightFn instanceof 
FunctionParseNode);
+    assertTrue("LHS function arg should be L2_DISTANCE node",
+      isDistance(leftFn.getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("RHS function arg should be COSINE_DISTANCE node",
+      isDistance(rightFn.getChildren().get(0), "COSINE_DISTANCE"));
+
+    // Weighted linear combination of distance metrics in ORDER BY clause
+    String sql2 = "SELECT doc_id FROM documents "
+      + "ORDER BY (embedding <=> :1) * 0.7 + (embedding <-> :2) * 0.3 ASC, 
doc_id DESC";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    List<OrderByNode> orderBy = select2.getOrderBy();
+    assertNotNull(orderBy);
+    assertEquals(2, orderBy.size());
+
+    OrderByNode order1 = orderBy.get(0);
+    assertTrue("First ORDER BY should be ascending", order1.isAscending());
+    ParseNode orderNode1 = order1.getNode();
+    assertTrue("Weighted combination should be AddParseNode", orderNode1 
instanceof AddParseNode);
+    assertEquals(2, orderNode1.getChildren().size());
+
+    ParseNode term1 = orderNode1.getChildren().get(0);
+    assertTrue("Term 1 should be MultiplyParseNode", term1 instanceof 
MultiplyParseNode);
+    assertTrue("Term 1 child 0 should be COSINE_DISTANCE node",
+      isDistance(term1.getChildren().get(0), "COSINE_DISTANCE"));
+
+    ParseNode term2 = orderNode1.getChildren().get(1);
+    assertTrue("Term 2 should be MultiplyParseNode", term2 instanceof 
MultiplyParseNode);
+    assertTrue("Term 2 child 0 should be L2_DISTANCE node",
+      isDistance(term2.getChildren().get(0), "L2_DISTANCE"));
+
+    OrderByNode order2 = orderBy.get(1);
+    assertTrue("Second ORDER BY should be descending", !order2.isAscending());
+    assertTrue("Second ORDER BY node should be ColumnParseNode",
+      order2.getNode() instanceof ColumnParseNode);
+  }
+
+  @Test
+  public void testIsNullWithDistanceExpression() throws Exception {
+    // IS NULL predicate with distance operand
+    String sql1 = "SELECT * FROM t WHERE v1 <-> v2 IS NULL";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode where1 = select1.getWhere();
+    assertTrue("WHERE should be IsNullParseNode", where1 instanceof 
IsNullParseNode);
+    assertTrue("Should be IS NULL (not negated)", !((IsNullParseNode) 
where1).isNegate());
+    assertTrue("Child of IS NULL should be L2_DISTANCE node",
+      isDistance(where1.getChildren().get(0), "L2_DISTANCE"));
+
+    // IS NOT NULL predicate with distance operand
+    String sql2 = "SELECT * FROM t WHERE v1 <=> v2 IS NOT NULL";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    ParseNode where2 = select2.getWhere();
+    assertTrue("WHERE should be IsNullParseNode", where2 instanceof 
IsNullParseNode);
+    assertTrue("Should be IS NOT NULL (negated)", ((IsNullParseNode) 
where2).isNegate());
+    assertTrue("Child of IS NOT NULL should be COSINE_DISTANCE node",
+      isDistance(where2.getChildren().get(0), "COSINE_DISTANCE"));
+  }
+
+  @Test
+  public void testInterleavedArithmeticAndDistancePrecedence() throws 
Exception {
+    // Unparenthesized expression evaluated according to operator precedence
+    String sql1 = "SELECT v1 <-> v2 + v3 <=> v4 FROM t";
+    SelectStatement select1 = (SelectStatement) new 
SQLParser(sql1).parseStatement();
+    ParseNode root1 = select1.getSelect().get(0).getNode();
+    assertTrue("Root should be COSINE_DISTANCE node", isDistance(root1, 
"COSINE_DISTANCE"));
+    assertEquals(2, root1.getChildren().size());
+
+    ParseNode lhsCos = root1.getChildren().get(0);
+    assertTrue("LHS of <=> should be L2_DISTANCE node", isDistance(lhsCos, 
"L2_DISTANCE"));
+    ParseNode rhsCos = root1.getChildren().get(1);
+    assertTrue("RHS of <=> should be ColumnParseNode", rhsCos instanceof 
ColumnParseNode);
+    assertEquals("V4", ((ColumnParseNode) rhsCos).getName());
+
+    assertEquals("V1", ((ColumnParseNode) 
lhsCos.getChildren().get(0)).getName());
+    ParseNode rhsL2 = lhsCos.getChildren().get(1);
+    assertTrue("RHS of <-> should be AddParseNode", rhsL2 instanceof 
AddParseNode);
+    assertEquals("V2", ((ColumnParseNode) 
rhsL2.getChildren().get(0)).getName());
+    assertEquals("V3", ((ColumnParseNode) 
rhsL2.getChildren().get(1)).getName());
+
+    // Parenthesized sub-expressions overriding default operator precedence
+    String sql2 = "SELECT (v1 <-> v2) + (v3 <=> v4) FROM t";
+    SelectStatement select2 = (SelectStatement) new 
SQLParser(sql2).parseStatement();
+    ParseNode root2 = select2.getSelect().get(0).getNode();
+    assertTrue("Root should be AddParseNode", root2 instanceof AddParseNode);
+    assertEquals(2, root2.getChildren().size());
+    assertTrue("LHS of + should be L2_DISTANCE node",
+      isDistance(root2.getChildren().get(0), "L2_DISTANCE"));
+    assertTrue("RHS of + should be COSINE_DISTANCE node",
+      isDistance(root2.getChildren().get(1), "COSINE_DISTANCE"));
+  }
+
+  /**
+   * Verifies that relational less-than comparisons remain unaffected by 
distance operator
+   * tokenization.
+   */
+  @Test
+  public void testLessThanComparisonsUnaffected() throws Exception {
+    String[][] cases =
+      { { "a<-1", "<" }, { "a < -1", "<" }, { "a<=b", "<=" }, { "a<>b", "!=" 
}, { "a<b", "<" } };
+    for (String[] c : cases) {
+      SelectStatement select =
+        (SelectStatement) new SQLParser("SELECT * FROM t WHERE " + 
c[0]).parseStatement();
+      ParseNode where = select.getWhere();
+      assertTrue(c[0] + " should parse as a comparison", where instanceof 
ComparisonParseNode);
+      assertEquals(c[0], c[1], QueryUtil.toSQL(((ComparisonParseNode) 
where).getFilterOp()));
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
 
b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
new file mode 100644
index 0000000000..bb21cc9f17
--- /dev/null
+++ 
b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.phoenix.expression.function;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Random;
+import org.apache.phoenix.schema.types.PVectorFloat;
+import org.junit.Test;
+
+/**
+ * Parity tests validating Panama Vector API distance kernel outputs against 
scalar reference
+ * implementations across dimensions and bound thresholds under the {@code 
java21} test profile.
+ */
+public class PanamaDistanceKernelParityTest {
+
+  private static final int[] DIMS = { 1, 3, 7, 8, 15, 16, 64, 100, 128, 129, 
768, 1536 };
+  private static final double TOLERANCE = 1e-5;
+
+  private static float[] random(Random rng, int dim) {
+    float[] v = new float[dim];
+    for (int i = 0; i < dim; i++) {
+      v[i] = (rng.nextFloat() - 0.5f) * 20.0f;
+    }
+    return v;
+  }
+
+  private static void assertClose(String what, double expected, double actual) 
{
+    double scale = Math.max(1.0, Math.abs(expected));
+    assertEquals(what, expected, actual, TOLERANCE * scale);
+  }
+
+  @Test
+  public void testSimdKernelsAreActive() {
+    assertTrue("Panama kernels should be active under the java21 profile",
+      VectorDistanceUtil.isSimdEnabled());
+  }
+
+  @Test
+  public void testKernelParity() {
+    Random rng = new Random(42);
+    for (int dim : DIMS) {
+      for (int trial = 0; trial < 50; trial++) {
+        float[] a = random(rng, dim);
+        float[] b = random(rng, dim);
+        String ctx = "dim=" + dim + " trial=" + trial;
+        assertClose("l2sq " + ctx,
+          ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE),
+          PanamaDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE));
+        assertClose("dot " + ctx, ScalarDistanceKernel.dotProduct(a, b, dim),
+          PanamaDistanceKernel.dotProduct(a, b, dim));
+        assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, 
dim),
+          PanamaDistanceKernel.cosineDistance(a, b, dim));
+      }
+    }
+  }
+
+  @Test
+  public void testPackedParity() {
+    Random rng = new Random(7);
+    for (int dim : DIMS) {
+      float[] a = random(rng, dim);
+      float[] b = random(rng, dim);
+      byte[] pa = PVectorFloat.INSTANCE.toBytes(a);
+      byte[] pb = PVectorFloat.INSTANCE.toBytes(b);
+      String ctx = "dim=" + dim;
+      assertClose("l2sq " + ctx, ScalarDistanceKernel.l2DistanceSquared(a, b, 
dim, Double.MAX_VALUE),
+        VectorDistanceUtil.l2DistanceSquaredWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+      assertClose("l2 " + ctx,
+        Math.sqrt(ScalarDistanceKernel.l2DistanceSquared(a, b, dim, 
Double.MAX_VALUE)),
+        VectorDistanceUtil.l2DistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+      assertClose("ip " + ctx, -ScalarDistanceKernel.dotProduct(a, b, dim),
+        VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+      assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, 
dim),
+        VectorDistanceUtil.cosineDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+    }
+  }
+
+  @Test
+  public void testBoundedL2Semantics() {
+    Random rng = new Random(11);
+    for (int dim : DIMS) {
+      float[] a = random(rng, dim);
+      float[] b = random(rng, dim);
+      double exact = ScalarDistanceKernel.l2DistanceSquared(a, b, dim, 
Double.MAX_VALUE);
+      assertEquals(Double.MAX_VALUE,
+        PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 0.5), 0.0);
+      assertClose("dim=" + dim, exact,
+        PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 1.5));
+    }
+  }
+}
diff --git a/pom.xml b/pom.xml
index 58f24340a8..c09428fe62 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2509,6 +2509,14 @@
           @{jacocoArgLine}</argLine>
       </properties>
     </profile>
+    <!-- Java 21 profile enabling Panama Vector API incubator module for test 
execution. -->
+    <profile>
+      <id>java21</id>
+      <properties>
+        <phoenix-surefire.jdk21.flags>-Djava.security.manager=allow
+          --add-modules jdk.incubator.vector</phoenix-surefire.jdk21.flags>
+      </properties>
+    </profile>
     <profile>
       <id>build-with-jdk21</id>
       <activation>

Reply via email to