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 6a97771b4e2b11fcac2954b4c7a38f8eaf32b22f
Author: Andrew Purtell <[email protected]>
AuthorDate: Wed Sep 16 23:23:12 2026 -0700

    PHOENIX-7998 Phase 1: Vector data type and SQL grammar
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 phoenix-core-client/src/main/antlr3/PhoenixSQL.g   |  44 +-
 .../apache/phoenix/compile/ProjectionCompiler.java |   6 +-
 .../apache/phoenix/exception/SQLExceptionCode.java |   1 +
 .../org/apache/phoenix/jdbc/PhoenixResultSet.java  |   9 +-
 .../java/org/apache/phoenix/parse/ColumnDef.java   |  40 +-
 .../org/apache/phoenix/schema/ValueSchema.java     |  14 +-
 .../phoenix/schema/types/PArrayDataType.java       |  17 +
 .../org/apache/phoenix/schema/types/PDataType.java |   6 +
 .../phoenix/schema/types/PDataTypeFactory.java     |  16 +
 .../phoenix/schema/types/PVectorDataType.java      | 385 ++++++++++++++
 .../apache/phoenix/schema/types/PVectorDouble.java | 137 +++++
 .../apache/phoenix/schema/types/PVectorFloat.java  | 137 +++++
 .../java/org/apache/phoenix/util/SchemaUtil.java   |   8 +-
 .../org/apache/phoenix/end2end/VectorColumnIT.java | 441 ++++++++++++++++
 .../phoenix/compile/VectorColumnCompileTest.java   | 128 +++++
 .../phoenix/expression/CoerceExpressionTest.java   |   2 +
 .../phoenix/parse/VectorColumnParseTest.java       | 222 ++++++++
 .../apache/phoenix/schema/types/PDataTypeTest.java |  31 +-
 .../phoenix/schema/types/VectorDataTypeTest.java   | 559 +++++++++++++++++++++
 19 files changed, 2159 insertions(+), 44 deletions(-)

diff --git a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g 
b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
index 768eb60249..c2c49ace51 100644
--- a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
+++ b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
@@ -224,6 +224,7 @@ import org.apache.phoenix.schema.PTable.IndexType;
 import org.apache.phoenix.schema.PTable.CDCChangeScope;
 import org.apache.phoenix.schema.stats.StatisticsCollectionScope;
 import org.apache.phoenix.schema.types.PDataType;
+import org.apache.phoenix.schema.types.PDataTypeFactory;
 import org.apache.phoenix.schema.types.PDate;
 import org.apache.phoenix.schema.types.PTime;
 import org.apache.phoenix.schema.types.PTimestamp;
@@ -885,16 +886,35 @@ indexes returns [List<NamedNode> ret]
     :  v = index_name {$ret.add(v);}  (COMMA v = index_name {$ret.add(v);} )*
 ;
 
+// A column type with an optional length and scale. A leading component type 
is valid only in
+// VECTOR(<component type>, <dimension>) with FLOAT or DOUBLE. Any other use 
causes a parse error.
+column_type returns [String name, Integer maxLength, Integer scale]
+    :   dt=identifier (LPAREN (ct=identifier COMMA)? l=NUMBER (COMMA 
s=NUMBER)? RPAREN)?
+        {
+            $name = dt;
+            $maxLength = l == null ? null : Integer.parseInt( l.getText() );
+            $scale = s == null ? null : Integer.parseInt( s.getText() );
+            if (ct != null) {
+                PDataType vectorType = "VECTOR".equalsIgnoreCase(dt) && s == 
null
+                    ? PDataTypeFactory.getInstance().typeForVector(ct) : null;
+                if (vectorType == null) {
+                    throw new ParseException("Unsupported type: " + dt + "(" + 
ct + ", ...)");
+                }
+                $name = vectorType.getSqlTypeName();
+            }
+        }
+    ;
+
 column_def returns [ColumnDef ret]
-    :   c=column_name dt=identifier (LPAREN l=NUMBER (COMMA s=NUMBER)? 
RPAREN)? ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? RSQUARE)? (nn=NOT? n=NULL)? 
(DEFAULT df=expression)? ((pk=PRIMARY KEY (order=ASC|order=DESC)? 
rr=ROW_TIMESTAMP?)|(ENCODED_QUALIFIER eq=NUMBER))?
+    :   c=column_name t=column_type ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? 
RSQUARE)? (nn=NOT? n=NULL)? (DEFAULT df=expression)? ((pk=PRIMARY KEY 
(order=ASC|order=DESC)? rr=ROW_TIMESTAMP?)|(ENCODED_QUALIFIER eq=NUMBER))?
         { $ret = factory.columnDef(
             c,
-            dt,
+            $t.name,
             ar != null || lsq != null,
             a == null ? null :  Integer.parseInt( a.getText() ),
             nn!=null ? Boolean.FALSE : n!=null ? Boolean.TRUE : null,
-            l == null ? null : Integer.parseInt( l.getText() ),
-            s == null ? null : Integer.parseInt( s.getText() ),
+            $t.maxLength,
+            $t.scale,
             pk != null, 
             order == null ? SortOrder.getDefault() : 
SortOrder.fromDDLValue(order.getText()),
             df == null ? null : df.toString(),
@@ -908,20 +928,20 @@ dyn_column_defs returns [List<ColumnDef> ret]
 ;
 
 dyn_column_def returns [ColumnDef ret]
-    :   c=column_name dt=identifier (LPAREN l=NUMBER (COMMA s=NUMBER)? 
RPAREN)? ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? RSQUARE)?
-        {$ret = factory.columnDef(c, dt, ar != null || lsq != null, a == null 
? null :  Integer.parseInt( a.getText() ), Boolean.TRUE,
-            l == null ? null : Integer.parseInt( l.getText() ),
-            s == null ? null : Integer.parseInt( s.getText() ),
+    :   c=column_name t=column_type ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? 
RSQUARE)?
+        {$ret = factory.columnDef(c, $t.name, ar != null || lsq != null, a == 
null ? null :  Integer.parseInt( a.getText() ), Boolean.TRUE,
+            $t.maxLength,
+            $t.scale,
             false, 
             SortOrder.getDefault(),
             false); }
     ;
 
 dyn_column_name_or_def returns [ColumnDef ret]
-    :   c=column_name (dt=identifier (LPAREN l=NUMBER (COMMA s=NUMBER)? 
RPAREN)? ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? RSQUARE)? )? 
-        {$ret = factory.columnDef(c, dt, ar != null || lsq != null, a == null 
? null :  Integer.parseInt( a.getText() ), Boolean.TRUE,
-            l == null ? null : Integer.parseInt( l.getText() ),
-            s == null ? null : Integer.parseInt( s.getText() ),
+    :   c=column_name (t=column_type ar=ARRAY? (lsq=LSQUARE (a=NUMBER)? 
RSQUARE)? )? 
+        {$ret = factory.columnDef(c, $t.name, ar != null || lsq != null, a == 
null ? null :  Integer.parseInt( a.getText() ), Boolean.TRUE,
+            $t.maxLength,
+            $t.scale,
             false, 
             SortOrder.getDefault(),
             false); }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
index 138f000e50..90844b5aac 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
@@ -666,9 +666,8 @@ public class ProjectionCompiler {
         PColumnFamily family = table.getColumnFamily(entry.getKey());
         if (entry.getValue() == null) {
           for (PColumn column : family.getColumns()) {
-            Integer maxLength = column.getMaxLength();
             int byteSize = column.getDataType().isFixedWidth()
-              ? maxLength == null ? column.getDataType().getByteSize() : 
maxLength
+              ? SchemaUtil.getFixedByteSize(column)
               : RowKeySchema.ESTIMATED_VARIABLE_LENGTH_SIZE;
             estimatedByteSize += SizedUtil.KEY_VALUE_SIZE + estimatedKeySize + 
byteSize;
           }
@@ -681,9 +680,8 @@ public class ProjectionCompiler {
             if (column == null) {
               continue;
             }
-            Integer maxLength = column.getMaxLength();
             int byteSize = column.getDataType().isFixedWidth()
-              ? maxLength == null ? column.getDataType().getByteSize() : 
maxLength
+              ? SchemaUtil.getFixedByteSize(column)
               : RowKeySchema.ESTIMATED_VARIABLE_LENGTH_SIZE;
             estimatedByteSize += SizedUtil.KEY_VALUE_SIZE + estimatedKeySize + 
byteSize;
           }
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 3eddf2278f..c4b33a7649 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
@@ -88,6 +88,7 @@ public enum SQLExceptionCode {
   DATA_EXCEEDS_MAX_CAPACITY(206, "22003", "The data exceeds the max capacity 
for the data type."),
   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."),
   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/jdbc/PhoenixResultSet.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixResultSet.java
index 6b8ae717de..5e058268eb 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixResultSet.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixResultSet.java
@@ -263,8 +263,10 @@ public class PhoenixResultSet implements 
PhoenixMonitoredResultSet, SQLCloseable
     // Get the value using the expected type instead of trying to coerce to 
VARCHAR.
     // We can't coerce using our formatter because we don't have enough 
context in PDataType.
     ColumnProjector projector = 
getRowProjector().getColumnProjector(columnIndex - 1);
-    Array value =
-      (Array) projector.getValue(currentRow, 
projector.getExpression().getDataType(), ptr);
+    PDataType type = projector.getExpression().getDataType();
+    Array value = (Array) (type != null && type.isVectorType()
+      ? projector.getValue(currentRow, type, ptr, java.sql.Array.class)
+      : projector.getValue(currentRow, type, ptr));
     wasNull = (value == null);
     return value;
   }
@@ -707,6 +709,9 @@ public class PhoenixResultSet implements 
PhoenixMonitoredResultSet, SQLCloseable
     // This provides a simple way of getting a reasonable string representation
     // for types like DATE and TIME
     Format formatter = statement.getFormatter(type);
+    if (type != null && type.isVectorType()) {
+      return type.toStringLiteral(value, formatter);
+    }
     return formatter == null ? value.toString() : formatter.format(value);
   }
 
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ColumnDef.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ColumnDef.java
index 1bfe53b3b9..0f5d24959a 100644
--- a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ColumnDef.java
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ColumnDef.java
@@ -33,6 +33,7 @@ import org.apache.phoenix.schema.SortOrder;
 import org.apache.phoenix.schema.types.PDataType;
 import org.apache.phoenix.schema.types.PDecimal;
 import org.apache.phoenix.schema.types.PVarbinary;
+import org.apache.phoenix.schema.types.PVectorDataType;
 import org.apache.phoenix.util.ExpressionUtil;
 import org.apache.phoenix.util.SchemaUtil;
 
@@ -130,6 +131,23 @@ public class ColumnDef {
           // ignored. All decimal are stored with as much decimal points as 
possible.
           scale = scale == null ? PDataType.DEFAULT_SCALE : scale > maxLength 
? maxLength : scale;
         }
+      } else if (baseType != null && baseType.isVectorType()) {
+        if (maxLength == null) {
+          throw new 
SQLExceptionInfo.Builder(SQLExceptionCode.MISSING_MAX_LENGTH)
+            
.setColumnName(columnDefName.getColumnName()).build().buildException();
+        }
+        if (maxLength < 1) {
+          throw new 
SQLExceptionInfo.Builder(SQLExceptionCode.NONPOSITIVE_MAX_LENGTH)
+            
.setColumnName(columnDefName.getColumnName()).build().buildException();
+        }
+        // Reject a dimension above the supported maximum.
+        if (maxLength > PVectorDataType.MAX_VECTOR_DIMENSION) {
+          throw new 
SQLExceptionInfo.Builder(SQLExceptionCode.VECTOR_DIMENSION_EXCEEDED)
+            .setColumnName(columnDefName.getColumnName()).setMessage("Vector 
dimension " + maxLength
+              + " exceeds maximum of " + PVectorDataType.MAX_VECTOR_DIMENSION)
+            .build().buildException();
+        }
+        scale = null;
       } else {
         if (maxLength != null && maxLength < 1) {
           throw new 
SQLExceptionInfo.Builder(SQLExceptionCode.NONPOSITIVE_MAX_LENGTH)
@@ -244,15 +262,21 @@ public class ColumnDef {
   public String toString() {
     StringBuilder buf = new 
StringBuilder(columnDefName.getColumnNode().toString());
     buf.append(' ');
-    buf.append(dataType.getSqlTypeName());
-    if (maxLength != null) {
-      buf.append('(');
-      buf.append(maxLength);
-      if (scale != null) {
-        buf.append(',');
-        buf.append(scale); // has both max length and scale. For ex- 
decimal(10,2)
+    if (dataType != null && dataType.isVectorType()) {
+      // Put the dimension into the type name VECTOR(<component type>) before 
the last parenthesis.
+      String typeName = dataType.getSqlTypeName();
+      buf.append(typeName, 0, typeName.length() - 1).append(", 
").append(maxLength).append(')');
+    } else {
+      buf.append(dataType.getSqlTypeName());
+      if (maxLength != null) {
+        buf.append('(');
+        buf.append(maxLength);
+        if (scale != null) {
+          buf.append(',');
+          buf.append(scale); // has both max length and scale. For ex- 
decimal(10,2)
+        }
+        buf.append(')');
       }
-      buf.append(')');
     }
     if (isArray) {
       buf.append(' ');
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/ValueSchema.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/ValueSchema.java
index 0947c4e609..f16fbf45ca 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/ValueSchema.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/ValueSchema.java
@@ -26,6 +26,7 @@ import java.util.List;
 import org.apache.hadoop.io.Writable;
 import org.apache.hadoop.io.WritableUtils;
 import org.apache.phoenix.schema.types.PDataType;
+import org.apache.phoenix.schema.types.PVectorDataType;
 import org.apache.phoenix.util.SizedUtil;
 
 import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions;
@@ -192,7 +193,10 @@ public abstract class ValueSchema implements Writable {
       this.isNullable = isNullable;
       if (this.type != null && this.type.isFixedWidth() && 
this.type.getByteSize() == null) {
         if (datum.getMaxLength() != null) {
-          this.byteSize = datum.getMaxLength();
+          // For a vector, the max length is the dimension, not the size in 
bytes.
+          this.byteSize = this.type.isVectorType()
+            ? this.type.estimateByteSizeFromLength(datum.getMaxLength())
+            : datum.getMaxLength();
         }
       }
     }
@@ -235,7 +239,13 @@ public abstract class ValueSchema implements Writable {
 
     @Override
     public Integer getMaxLength() {
-      return type.isFixedWidth() ? byteSize : null;
+      if (!type.isFixedWidth()) {
+        return null;
+      }
+      // For a vector, the max length is the dimension: the byte size divided 
by the element size.
+      return type.isVectorType()
+        ? byteSize / ((PVectorDataType) type).getElementByteSize()
+        : byteSize;
     }
 
     @Override
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PArrayDataType.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PArrayDataType.java
index 20dd8a6ff7..e800bea8bb 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PArrayDataType.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PArrayDataType.java
@@ -309,12 +309,29 @@ public abstract class PArrayDataType<T> extends 
PDataType<T> {
     throw new UnsupportedOperationException();
   }
 
+  /** Returns true if the element type of the array can be coerced to DOUBLE 
or DECIMAL. */
+  private static boolean isNumericArray(PDataType arrayType) {
+    PDataType elemType = arrayBaseType(arrayType);
+    return elemType.isCoercibleTo(PDouble.INSTANCE) || 
elemType.isCoercibleTo(PDecimal.INSTANCE);
+  }
+
+  @Override
+  public boolean isCastableTo(PDataType targetType) {
+    if (targetType != null && targetType.isVectorType()) {
+      return isNumericArray(this);
+    }
+    return super.isCastableTo(targetType);
+  }
+
   @Override
   public boolean isCoercibleTo(PDataType targetType, Object value) {
     return targetType.isCoercibleTo(targetType, value);
   }
 
   public boolean isCoercibleTo(PDataType targetType, PDataType 
expectedTargetType) {
+    if (targetType.isVectorType()) {
+      return isNumericArray(expectedTargetType);
+    }
     if (!targetType.isArrayType()) {
       return false;
     } else {
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataType.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataType.java
index cf1bcb93fb..91c902b82a 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataType.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataType.java
@@ -165,6 +165,10 @@ public abstract class PDataType<T> implements DataType<T>, 
Comparable<PDataType<
     return false;
   }
 
+  public boolean isVectorType() {
+    return false;
+  }
+
   public final int compareTo(byte[] lhs, int lhsOffset, int lhsLength, 
SortOrder lhsSortOrder,
     byte[] rhs, int rhsOffset, int rhsLength, SortOrder rhsSortOrder, 
PDataType rhsType) {
     Preconditions.checkNotNull(lhsSortOrder);
@@ -564,6 +568,8 @@ public abstract class PDataType<T> implements DataType<T>, 
Comparable<PDataType<
   public final static Integer DOUBLE_PRECISION = 15;
 
   public static final int ARRAY_TYPE_BASE = 3000;
+  public static final int VECTOR_FLOAT_TYPE = 4001;
+  public static final int VECTOR_DOUBLE_TYPE = 4002;
   public static final int JSON_TYPE = 5000;
   public static final int BSON_TYPE = 7000;
   public static final int VARBINARY_ENCODED_TYPE = 9000;
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataTypeFactory.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataTypeFactory.java
index 648426bd90..1f640a869d 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataTypeFactory.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PDataTypeFactory.java
@@ -126,6 +126,8 @@ public class PDataTypeFactory {
     types.add(PJson.INSTANCE);
     types.add(PBson.INSTANCE);
     types.add(PVarbinaryEncoded.INSTANCE);
+    types.add(PVectorFloat.INSTANCE);
+    types.add(PVectorDouble.INSTANCE);
 
     classToInstance = new HashMap<>(types.size());
     for (PDataType t : types) {
@@ -165,4 +167,18 @@ public class PDataTypeFactory {
       return javaClassToInstance.get(clazz);
     }
   }
+
+  /**
+   * Returns the vector type for the given component type name, FLOAT or 
DOUBLE, or null if no
+   * vector type supports that component type. The match ignores case and 
surrounding white space.
+   */
+  public PDataType typeForVector(String componentType) {
+    String normalized = componentType.trim().toUpperCase();
+    if (PFloat.INSTANCE.getSqlTypeName().equals(normalized)) {
+      return PVectorFloat.INSTANCE;
+    } else if (PDouble.INSTANCE.getSqlTypeName().equals(normalized)) {
+      return PVectorDouble.INSTANCE;
+    }
+    return null;
+  }
 }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
new file mode 100644
index 0000000000..8127decd1c
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
@@ -0,0 +1,385 @@
+/*
+ * 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.schema.types;
+
+import java.lang.reflect.Array;
+import java.sql.SQLException;
+import java.text.Format;
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.ConstraintViolationException;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.util.ByteUtil;
+
+/**
+ * Base class for fixed-dimension vector types {@code VECTOR(FLOAT, N)} and
+ * {@code VECTOR(DOUBLE, N)}.
+ * <p>
+ * Vectors are serialized as dense, contiguous IEEE 754 big-endian values 
without header metadata or
+ * null masks. Vector dimension is stored in column max length. Vectors do not 
support primary key
+ * usage or ordering comparisons.
+ * @param <T> the primitive array type of the vector, {@code float[]} or 
{@code double[]}
+ */
+public abstract class PVectorDataType<T> extends PDataType<T> {
+
+  /** The maximum dimension that a vector column definition can declare. */
+  public static final int MAX_VECTOR_DIMENSION = 65536;
+
+  protected PVectorDataType(String sqlTypeName, int sqlType, Class<T> clazz, 
int ordinal) {
+    super(sqlTypeName, sqlType, clazz, null, ordinal);
+  }
+
+  /** Returns the size in bytes of one vector element. */
+  public abstract int getElementByteSize();
+
+  /** Returns the scalar type of one vector element. */
+  public abstract PDataType getElementType();
+
+  /** Returns the number of elements in a vector of this type. */
+  protected abstract int length(T vector);
+
+  /** Creates a vector of this type with the given dimension. All elements are 
zero. */
+  protected abstract T allocate(int dimension);
+
+  /** Sets one element of the vector to a numeric value, converted to the 
element type. */
+  protected abstract void set(T vector, int index, Number value);
+
+  /** Returns one element of the vector as a boxed number. */
+  protected abstract Number get(T vector, int index);
+
+  /** Parses one element from its string form. */
+  protected abstract Number parseElement(String s);
+
+  /** Packs a vector into a byte buffer as contiguous big-endian IEEE 754 
values. */
+  protected abstract void pack(T vector, byte[] buf, int offset);
+
+  /** Unpacks a vector from a packed byte range that the writer encoded in the 
given sort order. */
+  protected abstract T unpack(byte[] buf, int offset, int length, SortOrder 
sortOrder);
+
+  /** Returns the index of the first NaN or infinite element, or -1 if all 
elements are finite. */
+  protected abstract int indexOfNonFinite(T vector);
+
+  @Override
+  public boolean isVectorType() {
+    return true;
+  }
+
+  @Override
+  public boolean isFixedWidth() {
+    return true;
+  }
+
+  @Override
+  public boolean canBePrimaryKey() {
+    return false;
+  }
+
+  @Override
+  public boolean isComparisonSupported() {
+    return false;
+  }
+
+  @Override
+  public Integer getByteSize() {
+    return null;
+  }
+
+  @Override
+  public int estimateByteSize(Object o) {
+    Integer dim = getMaxLength(o);
+    return dim == null ? 0 : dim * getElementByteSize();
+  }
+
+  @Override
+  public Integer estimateByteSizeFromLength(Integer length) {
+    return length == null ? null : length * getElementByteSize();
+  }
+
+  @Override
+  public Integer getMaxLength(Object o) {
+    if (o == null) {
+      return null;
+    }
+    if (o instanceof PhoenixArray) {
+      return ((PhoenixArray) o).getDimensions();
+    }
+    if (o instanceof java.sql.Array) {
+      o = getArray((java.sql.Array) o);
+    }
+    return o != null && o.getClass().isArray() ? Array.getLength(o) : null;
+  }
+
+  @Override
+  public int compareTo(Object lhs, Object rhs, PDataType rhsType) {
+    if (lhs == rhs) {
+      return 0;
+    }
+    if (lhs == null) {
+      return -1;
+    }
+    if (rhs == null) {
+      return 1;
+    }
+    byte[] rhsBytes = rhsType == this ? packed(asVector(rhs)) : 
rhsType.toBytes(rhs);
+    return Bytes.compareTo(packed(asVector(lhs)), rhsBytes);
+  }
+
+  @Override
+  public byte[] toBytes(Object object) {
+    if (object == null) {
+      return ByteUtil.EMPTY_BYTE_ARRAY;
+    }
+    return packed(toVector(object));
+  }
+
+  private byte[] packed(T vector) {
+    byte[] bytes = new byte[length(vector) * getElementByteSize()];
+    pack(vector, bytes, 0);
+    return bytes;
+  }
+
+  @Override
+  public int toBytes(Object object, byte[] bytes, int offset) {
+    if (object == null) {
+      return 0;
+    }
+    T vector = toVector(object);
+    pack(vector, bytes, offset);
+    return length(vector) * getElementByteSize();
+  }
+
+  @Override
+  public byte[] toBytes(Object object, SortOrder sortOrder) {
+    byte[] bytes = toBytes(object);
+    if (sortOrder == SortOrder.DESC) {
+      SortOrder.invert(bytes, 0, bytes, 0, bytes.length);
+    }
+    return bytes;
+  }
+
+  @Override
+  public T toObject(byte[] bytes, int offset, int length, PDataType 
actualType, SortOrder sortOrder,
+    Integer maxLength, Integer scale) {
+    if (bytes == null || length == 0) {
+      return null;
+    }
+    return unpack(bytes, offset, length, sortOrder == null ? 
SortOrder.getDefault() : sortOrder);
+  }
+
+  @Override
+  public Object toObject(byte[] bytes, int offset, int length, PDataType 
actualType,
+    SortOrder sortOrder, Integer maxLength, Integer scale, Class jdbcType) 
throws SQLException {
+    if (getJavaClass().isAssignableFrom(jdbcType) || 
Object.class.equals(jdbcType)) {
+      return toObject(bytes, offset, length, actualType, sortOrder, maxLength, 
scale);
+    }
+    boolean boxed = jdbcType.isArray() && 
!jdbcType.getComponentType().isPrimitive();
+    if (boxed || java.sql.Array.class.isAssignableFrom(jdbcType)) {
+      T raw = toObject(bytes, offset, length, actualType, sortOrder, 
maxLength, scale);
+      if (raw == null) {
+        return null;
+      }
+      Object[] elements =
+        (Object[]) Array.newInstance(getElementType().getJavaClass(), 
length(raw));
+      for (int i = 0; i < elements.length; i++) {
+        elements[i] = get(raw, i);
+      }
+      return boxed ? elements : 
PArrayDataType.instantiatePhoenixArray(getElementType(), elements);
+    }
+    throw newMismatchException(actualType, jdbcType);
+  }
+
+  @Override
+  public Object toObject(String value) {
+    if (value == null) {
+      return null;
+    }
+    String str = value.trim();
+    if (str.isEmpty()) {
+      return null;
+    }
+    if (str.startsWith("[") && str.endsWith("]")) {
+      str = str.substring(1, str.length() - 1).trim();
+    }
+    if (str.isEmpty()) {
+      return allocate(0);
+    }
+    String[] parts = str.split(",");
+    T vector = allocate(parts.length);
+    for (int i = 0; i < parts.length; i++) {
+      set(vector, i, parseElement(parts[i].trim()));
+    }
+    return vector;
+  }
+
+  /**
+   * Converts a value to a vector of this type. The value can be a vector of 
this type, a primitive
+   * or boxed numeric array, a Phoenix or JDBC array, or a string. Array 
elements must be numeric
+   * and not null, and all elements must be finite. A value that does not obey 
these rules causes a
+   * {@link ConstraintViolationException}. A string element that is not a 
number causes a
+   * {@link NumberFormatException}.
+   */
+  @Override
+  @SuppressWarnings("unchecked")
+  public Object toObject(Object object, PDataType actualType) {
+    if (object == null) {
+      return null;
+    }
+    if (getJavaClass().isInstance(object)) {
+      return checkFinite((T) object);
+    }
+    if (object instanceof java.sql.Array) {
+      object = getArray((java.sql.Array) object);
+    }
+    if (object.getClass().isArray()) {
+      int dim = Array.getLength(object);
+      T vector = allocate(dim);
+      for (int i = 0; i < dim; i++) {
+        Object e = Array.get(object, i);
+        if (e == null) {
+          throw new ConstraintViolationException("Vector elements must not be 
NULL");
+        }
+        if (!(e instanceof Number)) {
+          return throwConstraintViolationException(actualType, this);
+        }
+        set(vector, i, (Number) e);
+      }
+      return checkFinite(vector);
+    }
+    if (object instanceof String) {
+      return checkFinite((T) toObject((String) object));
+    }
+    return throwConstraintViolationException(actualType, this);
+  }
+
+  /**
+   * A vector can be cast only to a type that it can be coerced to. The 
inherited check uses
+   * comparability, which also accepts a cast from a vector to an array. That 
cast has no
+   * conversion, because the coercion from an array to a vector goes in one 
direction only.
+   */
+  @Override
+  public boolean isCastableTo(PDataType targetType) {
+    return isCoercibleTo(targetType);
+  }
+
+  @Override
+  public boolean isCoercibleTo(PDataType targetType, Object value) {
+    return value == null || isCoercibleTo(targetType);
+  }
+
+  @Override
+  public boolean isBytesComparableWith(PDataType otherType) {
+    return this.equals(otherType);
+  }
+
+  @Override
+  public boolean isSizeCompatible(ImmutableBytesWritable ptr, Object value, 
PDataType srcType,
+    SortOrder sortOrder, Integer maxLength, Integer scale, Integer 
desiredMaxLength,
+    Integer desiredScale) {
+    if (desiredMaxLength == null) {
+      return true;
+    }
+    Integer actualDimension = getMaxLength(value);
+    if (actualDimension != null) {
+      checkDimension(desiredMaxLength, actualDimension);
+    }
+    if (ptr != null && ptr.getLength() != 0 && srcType == this) {
+      checkDimension(desiredMaxLength, ptr.getLength() / getElementByteSize());
+    }
+    return true;
+  }
+
+  @Override
+  public void coerceBytes(ImmutableBytesWritable ptr, Object o, PDataType 
actualType,
+    Integer actualMaxLength, Integer actualScale, SortOrder actualModifier,
+    Integer desiredMaxLength, Integer desiredScale, SortOrder 
expectedModifier) {
+    super.coerceBytes(ptr, o, actualType, actualMaxLength, actualScale, 
actualModifier,
+      desiredMaxLength, desiredScale, expectedModifier);
+    if (ptr.getLength() > 0 && desiredMaxLength != null) {
+      checkDimension(desiredMaxLength, ptr.getLength() / getElementByteSize());
+    }
+  }
+
+  @Override
+  public Object getSampleValue(Integer maxLength, Integer arrayLength) {
+    int dim = maxLength == null ? (arrayLength == null ? 3 : arrayLength) : 
maxLength;
+    T sample = allocate(dim);
+    for (int i = 0; i < dim; i++) {
+      set(sample, i, RANDOM.get().nextFloat());
+    }
+    return sample;
+  }
+
+  @Override
+  public String toStringLiteral(Object o, Format formatter) {
+    if (o == null) {
+      return String.valueOf((Object) null);
+    }
+    T vector = asVector(o);
+    StringBuilder buf = new StringBuilder("[");
+    for (int i = 0; i < length(vector); i++) {
+      if (i > 0) {
+        buf.append(", ");
+      }
+      buf.append(get(vector, i));
+    }
+    return buf.append(']').toString();
+  }
+
+  @SuppressWarnings("unchecked")
+  private T toVector(Object object) {
+    return (T) toObject(object, this);
+  }
+
+  /**
+   * Returns a value that is already a vector of this type without change. 
Thus the render or
+   * comparison of a stored vector does not scan or reject its elements again. 
Other values go
+   * through the conversion and finite checks of {@link #toObject(Object, 
PDataType)}.
+   */
+  @SuppressWarnings("unchecked")
+  private T asVector(Object object) {
+    return getJavaClass().isInstance(object) ? (T) object : toVector(object);
+  }
+
+  /**
+   * Throws a ConstraintViolationException if the vector has a NaN or infinite 
element, because no
+   * distance metric can rank such a vector. A null vector passes.
+   */
+  private T checkFinite(T vector) {
+    int i = vector == null ? -1 : indexOfNonFinite(vector);
+    if (i >= 0) {
+      throw new ConstraintViolationException(
+        "Vector elements must be finite, but element " + i + " is " + 
get(vector, i));
+    }
+    return vector;
+  }
+
+  private static void checkDimension(int expected, int actual) {
+    if (actual != expected) {
+      throw new ConstraintViolationException(
+        "Vector dimension mismatch: expected " + expected + ", but got " + 
actual);
+    }
+  }
+
+  private static Object getArray(java.sql.Array array) {
+    try {
+      return array.getArray();
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
new file mode 100644
index 0000000000..b6219ec928
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
@@ -0,0 +1,137 @@
+/*
+ * 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.schema.types;
+
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.SortOrder;
+
+/**
+ * Data type representing fixed-dimension double-precision floating point 
vectors,
+ * {@code VECTOR(DOUBLE, N)}. Stored as packed big-endian IEEE 754 values.
+ */
+public class PVectorDouble extends PVectorDataType<double[]> {
+
+  public static final PVectorDouble INSTANCE = new PVectorDouble();
+
+  private PVectorDouble() {
+    super("VECTOR(DOUBLE)", PDataType.VECTOR_DOUBLE_TYPE, double[].class, 52);
+  }
+
+  /** Reads the element at the given index from packed vector bytes in ASC 
sort order. */
+  public static double readElement(byte[] buf, int offset, int index) {
+    return Bytes.toDouble(buf, offset + index * Bytes.SIZEOF_DOUBLE);
+  }
+
+  /** Reads the element at the given index from packed vector bytes in the 
given sort order. */
+  public static double readElement(byte[] buf, int offset, int index, 
SortOrder sortOrder) {
+    long b = Bytes.toLong(buf, offset + index * Bytes.SIZEOF_DOUBLE);
+    if (sortOrder == SortOrder.DESC) {
+      b ^= 0xFFFFFFFFFFFFFFFFL;
+    }
+    return Double.longBitsToDouble(b);
+  }
+
+  /** Reads the element at the given index from a packed vector pointer in ASC 
sort order. */
+  public static double readElement(ImmutableBytesWritable ptr, int index) {
+    return readElement(ptr.get(), ptr.getOffset(), index);
+  }
+
+  /** Decodes all elements from a packed vector byte range in ASC sort order. 
*/
+  public static double[] readElements(byte[] buf, int offset, int length) {
+    return readElements(buf, offset, length, SortOrder.ASC);
+  }
+
+  /** Decodes all elements from a packed vector byte range in the given sort 
order. */
+  public static double[] readElements(byte[] buf, int offset, int length, 
SortOrder sortOrder) {
+    double[] out = new double[length / Bytes.SIZEOF_DOUBLE];
+    for (int i = 0; i < out.length; i++) {
+      out[i] = readElement(buf, offset, i, sortOrder);
+    }
+    return out;
+  }
+
+  /**
+   * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 
754 values.
+   * @throws IllegalArgumentException if the buffer is too small to contain 
the vector
+   */
+  public static void writeElements(double[] vector, byte[] buf, int offset) {
+    if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_DOUBLE) {
+      throw new IllegalArgumentException("Buffer too small: need "
+        + (offset + vector.length * Bytes.SIZEOF_DOUBLE) + " bytes, have " + 
buf.length);
+    }
+    for (int i = 0; i < vector.length; i++) {
+      Bytes.putDouble(buf, offset + i * Bytes.SIZEOF_DOUBLE, vector[i]);
+    }
+  }
+
+  @Override
+  public int getElementByteSize() {
+    return Bytes.SIZEOF_DOUBLE;
+  }
+
+  @Override
+  public PDataType getElementType() {
+    return PDouble.INSTANCE;
+  }
+
+  @Override
+  protected int length(double[] vector) {
+    return vector.length;
+  }
+
+  @Override
+  protected double[] allocate(int dimension) {
+    return new double[dimension];
+  }
+
+  @Override
+  protected void set(double[] vector, int index, Number value) {
+    vector[index] = value.doubleValue();
+  }
+
+  @Override
+  protected Number get(double[] vector, int index) {
+    return vector[index];
+  }
+
+  @Override
+  protected Number parseElement(String s) {
+    return Double.valueOf(s);
+  }
+
+  @Override
+  protected void pack(double[] vector, byte[] buf, int offset) {
+    writeElements(vector, buf, offset);
+  }
+
+  @Override
+  protected double[] unpack(byte[] buf, int offset, int length, SortOrder 
sortOrder) {
+    return readElements(buf, offset, length, sortOrder);
+  }
+
+  @Override
+  protected int indexOfNonFinite(double[] vector) {
+    for (int i = 0; i < vector.length; i++) {
+      if (!Double.isFinite(vector[i])) {
+        return i;
+      }
+    }
+    return -1;
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
new file mode 100644
index 0000000000..814e48c044
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
@@ -0,0 +1,137 @@
+/*
+ * 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.schema.types;
+
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.SortOrder;
+
+/**
+ * Data type representing fixed-dimension single-precision floating point 
vectors,
+ * {@code VECTOR(FLOAT, N)}. Stored as packed big-endian IEEE 754 values.
+ */
+public class PVectorFloat extends PVectorDataType<float[]> {
+
+  public static final PVectorFloat INSTANCE = new PVectorFloat();
+
+  private PVectorFloat() {
+    super("VECTOR(FLOAT)", PDataType.VECTOR_FLOAT_TYPE, float[].class, 51);
+  }
+
+  /** Reads the element at the given index from packed vector bytes in ASC 
sort order. */
+  public static float readElement(byte[] buf, int offset, int index) {
+    return Bytes.toFloat(buf, offset + index * Bytes.SIZEOF_FLOAT);
+  }
+
+  /** Reads the element at the given index from packed vector bytes in the 
given sort order. */
+  public static float readElement(byte[] buf, int offset, int index, SortOrder 
sortOrder) {
+    int b = Bytes.toInt(buf, offset + index * Bytes.SIZEOF_FLOAT);
+    if (sortOrder == SortOrder.DESC) {
+      b ^= 0xFFFFFFFF;
+    }
+    return Float.intBitsToFloat(b);
+  }
+
+  /** Reads the element at the given index from a packed vector pointer in ASC 
sort order. */
+  public static float readElement(ImmutableBytesWritable ptr, int index) {
+    return readElement(ptr.get(), ptr.getOffset(), index);
+  }
+
+  /** Decodes all elements from a packed vector byte range in ASC sort order. 
*/
+  public static float[] readElements(byte[] buf, int offset, int length) {
+    return readElements(buf, offset, length, SortOrder.ASC);
+  }
+
+  /** Decodes all elements from a packed vector byte range in the given sort 
order. */
+  public static float[] readElements(byte[] buf, int offset, int length, 
SortOrder sortOrder) {
+    float[] out = new float[length / Bytes.SIZEOF_FLOAT];
+    for (int i = 0; i < out.length; i++) {
+      out[i] = readElement(buf, offset, i, sortOrder);
+    }
+    return out;
+  }
+
+  /**
+   * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 
754 values.
+   * @throws IllegalArgumentException if the buffer is too small to contain 
the vector
+   */
+  public static void writeElements(float[] vector, byte[] buf, int offset) {
+    if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_FLOAT) {
+      throw new IllegalArgumentException("Buffer too small: need "
+        + (offset + vector.length * Bytes.SIZEOF_FLOAT) + " bytes, have " + 
buf.length);
+    }
+    for (int i = 0; i < vector.length; i++) {
+      Bytes.putFloat(buf, offset + i * Bytes.SIZEOF_FLOAT, vector[i]);
+    }
+  }
+
+  @Override
+  public int getElementByteSize() {
+    return Bytes.SIZEOF_FLOAT;
+  }
+
+  @Override
+  public PDataType getElementType() {
+    return PFloat.INSTANCE;
+  }
+
+  @Override
+  protected int length(float[] vector) {
+    return vector.length;
+  }
+
+  @Override
+  protected float[] allocate(int dimension) {
+    return new float[dimension];
+  }
+
+  @Override
+  protected void set(float[] vector, int index, Number value) {
+    vector[index] = value.floatValue();
+  }
+
+  @Override
+  protected Number get(float[] vector, int index) {
+    return vector[index];
+  }
+
+  @Override
+  protected Number parseElement(String s) {
+    return Float.valueOf(s);
+  }
+
+  @Override
+  protected void pack(float[] vector, byte[] buf, int offset) {
+    writeElements(vector, buf, offset);
+  }
+
+  @Override
+  protected float[] unpack(byte[] buf, int offset, int length, SortOrder 
sortOrder) {
+    return readElements(buf, offset, length, sortOrder);
+  }
+
+  @Override
+  protected int indexOfNonFinite(float[] vector) {
+    for (int i = 0; i < vector.length; i++) {
+      if (!Float.isFinite(vector[i])) {
+        return i;
+      }
+    }
+    return -1;
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/SchemaUtil.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/SchemaUtil.java
index 81057f305f..8738c686ca 100644
--- a/phoenix-core-client/src/main/java/org/apache/phoenix/util/SchemaUtil.java
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/util/SchemaUtil.java
@@ -905,7 +905,13 @@ public class SchemaUtil {
   public static int getFixedByteSize(PDatum e) {
     assert (e.getDataType().isFixedWidth());
     Integer maxLength = e.getMaxLength();
-    return maxLength == null ? e.getDataType().getByteSize() : maxLength;
+    if (maxLength == null) {
+      return e.getDataType().getByteSize();
+    }
+    // For a vector, the max length is the dimension, not the size in bytes.
+    return e.getDataType().isVectorType()
+      ? e.getDataType().estimateByteSizeFromLength(maxLength)
+      : maxLength;
   }
 
   public static short getMaxKeySeq(PTable table) {
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/VectorColumnIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/VectorColumnIT.java
new file mode 100644
index 0000000000..1385892350
--- /dev/null
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/VectorColumnIT.java
@@ -0,0 +1,441 @@
+/*
+ * 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.end2end;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Array;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import org.apache.phoenix.exception.SQLExceptionCode;
+import org.apache.phoenix.jdbc.PhoenixConnection;
+import org.apache.phoenix.schema.PColumn;
+import org.apache.phoenix.schema.PTable;
+import org.apache.phoenix.schema.PTableKey;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(ParallelStatsDisabledTest.class)
+public class VectorColumnIT extends ParallelStatsDisabledIT {
+
+  @Test
+  public void testVectorDimensionPersistenceRoundTrip() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 128))");
+      PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+      PTable pTable = pconn.getTable(new PTableKey(null, tableName));
+      assertNotNull(pTable);
+      PColumn pColumn = pTable.getColumnForColumnName("V");
+      assertNotNull(pColumn);
+      assertEquals(Integer.valueOf(128), pColumn.getMaxLength());
+      assertEquals(PVectorFloat.INSTANCE, pColumn.getDataType());
+
+      // Read the metadata again from the system catalog, without the client 
cache.
+      try (Connection conn2 = DriverManager.getConnection(getUrl())) {
+        PhoenixConnection pconn2 = conn2.unwrap(PhoenixConnection.class);
+        PTable pTableFresh = pconn2.getTableNoCache(tableName);
+        assertNotNull(pTableFresh);
+        PColumn pColumnFresh = pTableFresh.getColumnForColumnName("V");
+        assertNotNull(pColumnFresh);
+        assertEquals(Integer.valueOf(128), pColumnFresh.getMaxLength());
+        assertEquals(PVectorFloat.INSTANCE, pColumnFresh.getDataType());
+      }
+
+      // Create the table again with dimension 256.
+      conn.createStatement().execute("DROP TABLE " + tableName);
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 256))");
+      pTable = pconn.getTable(new PTableKey(null, tableName));
+      assertNotNull(pTable);
+      pColumn = pTable.getColumnForColumnName("V");
+      assertNotNull(pColumn);
+      assertEquals(Integer.valueOf(256), pColumn.getMaxLength());
+      assertEquals(PVectorFloat.INSTANCE, pColumn.getDataType());
+
+      // An uncached read must show the new dimension.
+      try (Connection conn3 = DriverManager.getConnection(getUrl())) {
+        PhoenixConnection pconn3 = conn3.unwrap(PhoenixConnection.class);
+        PTable pTableFresh256 = pconn3.getTableNoCache(tableName);
+        assertNotNull(pTableFresh256);
+        PColumn pColumnFresh256 = pTableFresh256.getColumnForColumnName("V");
+        assertNotNull(pColumnFresh256);
+        assertEquals(Integer.valueOf(256), pColumnFresh256.getMaxLength());
+        assertEquals(PVectorFloat.INSTANCE, pColumnFresh256.getDataType());
+      }
+    }
+  }
+
+  @Test
+  public void testJdbcMetadataReporting() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 128))");
+
+      DatabaseMetaData dbmd = conn.getMetaData();
+      try (ResultSet rs = dbmd.getColumns(null, "", tableName, "V")) {
+        assertTrue(rs.next());
+        assertEquals("V", rs.getString("COLUMN_NAME"));
+        assertEquals(PVectorFloat.INSTANCE.getSqlType(), 
rs.getInt("DATA_TYPE"));
+        assertEquals("VECTOR(FLOAT)", rs.getString("TYPE_NAME"));
+        assertEquals(128, rs.getInt("COLUMN_SIZE"));
+      }
+
+      try (ResultSet rs = conn.createStatement().executeQuery("SELECT v FROM " 
+ tableName)) {
+        ResultSetMetaData rsmd = rs.getMetaData();
+        String colTypeName = rsmd.getColumnTypeName(1);
+        assertNotNull(colTypeName);
+        assertTrue("Column type name should contain VECTOR: " + colTypeName,
+          colTypeName.contains("VECTOR"));
+      }
+    }
+  }
+
+  @Test
+  public void testVectorDoubleDimensionPersistence() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(DOUBLE, 64))");
+      PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+      PTable pTable = pconn.getTable(new PTableKey(null, tableName));
+      assertNotNull(pTable);
+      PColumn pColumn = pTable.getColumnForColumnName("V");
+      assertNotNull(pColumn);
+      assertEquals(Integer.valueOf(64), pColumn.getMaxLength());
+      assertEquals(PVectorDouble.INSTANCE, pColumn.getDataType());
+
+      DatabaseMetaData dbmd = conn.getMetaData();
+      try (ResultSet rs = dbmd.getColumns(null, "", tableName, "V")) {
+        assertTrue(rs.next());
+        assertEquals("VECTOR(DOUBLE)", rs.getString("TYPE_NAME"));
+        assertEquals(64, rs.getInt("COLUMN_SIZE"));
+        assertEquals(PVectorDouble.INSTANCE.getSqlType(), 
rs.getInt("DATA_TYPE"));
+      }
+    }
+  }
+
+  @Test
+  public void testMultipleVectorColumnsPersistence() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute("CREATE TABLE " + tableName
+        + " (pk INTEGER PRIMARY KEY, vf VECTOR(FLOAT, 512), vd VECTOR(DOUBLE, 
1536))");
+      PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+      PTable pTable = pconn.getTable(new PTableKey(null, tableName));
+      assertNotNull(pTable);
+
+      PColumn colF = pTable.getColumnForColumnName("VF");
+      assertNotNull(colF);
+      assertEquals(Integer.valueOf(512), colF.getMaxLength());
+      assertEquals(PVectorFloat.INSTANCE, colF.getDataType());
+
+      PColumn colD = pTable.getColumnForColumnName("VD");
+      assertNotNull(colD);
+      assertEquals(Integer.valueOf(1536), colD.getMaxLength());
+      assertEquals(PVectorDouble.INSTANCE, colD.getDataType());
+    }
+  }
+
+  @Test
+  public void testJdbcArrayBindingRoundTrip() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+
+      String upsertSql = "UPSERT INTO " + tableName + " VALUES (?, ?)";
+      try (PreparedStatement ps = conn.prepareStatement(upsertSql)) {
+        ps.setInt(1, 1);
+        Array array = conn.createArrayOf("FLOAT", new Float[] { 1.0f, 2.0f, 
3.0f });
+        ps.setArray(2, array);
+        ps.executeUpdate();
+      }
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE pk = 1")) {
+        assertTrue(rs.next());
+        float[] retrieved = (float[]) rs.getObject(1);
+        assertNotNull(retrieved);
+        assertArrayEquals(new float[] { 1.0f, 2.0f, 3.0f }, retrieved, 
0.00001f);
+
+        Array retrievedArray = rs.getArray(1);
+        assertNotNull(retrievedArray);
+        Object arrayObj = retrievedArray.getArray();
+        if (arrayObj instanceof Float[]) {
+          assertArrayEquals(new Float[] { 1.0f, 2.0f, 3.0f }, (Float[]) 
arrayObj);
+        } else if (arrayObj instanceof float[]) {
+          assertArrayEquals(new float[] { 1.0f, 2.0f, 3.0f }, (float[]) 
arrayObj, 0.00001f);
+        }
+
+        String str = rs.getString(1);
+        assertNotNull(str);
+        assertTrue("Expected string representation containing numbers: " + str,
+          str.contains("1.0") && str.contains("2.0") && str.contains("3.0"));
+      }
+    }
+  }
+
+  @Test
+  public void testDimensionMismatchRejection() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+
+      String upsertSql = "UPSERT INTO " + tableName + " VALUES (?, ?)";
+      try (PreparedStatement ps = conn.prepareStatement(upsertSql)) {
+        ps.setInt(1, 1);
+        Array array = conn.createArrayOf("FLOAT", new Float[] { 1.0f, 2.0f, 
3.0f, 4.0f });
+        ps.setArray(2, array);
+        try {
+          ps.executeUpdate();
+          fail("Should have thrown exception on dimension mismatch");
+        } catch (SQLException e) {
+          assertEquals(SQLExceptionCode.CONSTRAINT_VIOLATION.getErrorCode(), 
e.getErrorCode());
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testArrayConstructorSyntax() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+
+      conn.createStatement()
+        .execute("UPSERT INTO " + tableName + " VALUES (1, ARRAY[1.0, 2.0, 
3.0])");
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE pk = 1")) {
+        assertTrue(rs.next());
+        float[] retrieved = (float[]) rs.getObject(1);
+        assertNotNull(retrieved);
+        assertArrayEquals(new float[] { 1.0f, 2.0f, 3.0f }, retrieved, 
0.00001f);
+      }
+    }
+  }
+
+  @Test
+  public void testJsonStringLiteralBinding() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+
+      conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES (1, 
'[4.0, 5.0, 6.0]')");
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE pk = 1")) {
+        assertTrue(rs.next());
+        float[] retrieved = (float[]) rs.getObject(1);
+        assertNotNull(retrieved);
+        assertArrayEquals(new float[] { 4.0f, 5.0f, 6.0f }, retrieved, 
0.00001f);
+      }
+    }
+  }
+
+  @Test
+  public void testVectorDoubleArrayBindingRoundTrip() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(DOUBLE, 3))");
+
+      String upsertSql = "UPSERT INTO " + tableName + " VALUES (?, ?)";
+      try (PreparedStatement ps = conn.prepareStatement(upsertSql)) {
+        ps.setInt(1, 1);
+        Array array = conn.createArrayOf("DOUBLE", new Double[] { 1.1, 2.2, 
3.3 });
+        ps.setArray(2, array);
+        ps.executeUpdate();
+      }
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE pk = 1")) {
+        assertTrue(rs.next());
+        double[] retrieved = (double[]) rs.getObject(1);
+        assertNotNull(retrieved);
+        assertArrayEquals(new double[] { 1.1, 2.2, 3.3 }, retrieved, 0.000001);
+      }
+    }
+  }
+
+  @Test
+  public void testAlterTableAddVectorColumn() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, name 
VARCHAR)");
+      conn.createStatement().execute("ALTER TABLE " + tableName + " ADD v 
VECTOR(FLOAT, 256)");
+
+      PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+      PTable pTable = pconn.getTable(new PTableKey(null, tableName));
+      PColumn pColumn = pTable.getColumnForColumnName("V");
+      assertNotNull(pColumn);
+      assertEquals(PVectorFloat.INSTANCE, pColumn.getDataType());
+      assertEquals(Integer.valueOf(256), pColumn.getMaxLength());
+
+      DatabaseMetaData dbmd = conn.getMetaData();
+      try (ResultSet rs = dbmd.getColumns(null, "", tableName, "V")) {
+        assertTrue(rs.next());
+        assertEquals("VECTOR(FLOAT)", rs.getString("TYPE_NAME"));
+        assertEquals(256, rs.getInt("COLUMN_SIZE"));
+      }
+
+      String upsertSql = "UPSERT INTO " + tableName + " VALUES (?, ?, ?)";
+      try (PreparedStatement ps = conn.prepareStatement(upsertSql)) {
+        ps.setInt(1, 1);
+        ps.setString(2, "test");
+        Array array = conn.createArrayOf("FLOAT", new Float[] { 1.0f, 2.0f, 
3.0f });
+        Float[] vec = new Float[256];
+        for (int i = 0; i < 256; i++) {
+          vec[i] = (float) i;
+        }
+        ps.setArray(3, conn.createArrayOf("FLOAT", vec));
+        ps.executeUpdate();
+      }
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT v FROM " + tableName + " 
WHERE pk = 1")) {
+        assertTrue(rs.next());
+        float[] retrieved = (float[]) rs.getObject(1);
+        assertNotNull(retrieved);
+        assertEquals(256, retrieved.length);
+        assertEquals(0.0f, retrieved[0], 0.00001f);
+        assertEquals(255.0f, retrieved[255], 0.00001f);
+      }
+    }
+  }
+
+  @Test
+  public void testAlterTableAddVectorDoubleColumn() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, name 
VARCHAR)");
+
+      conn.createStatement().execute("ALTER TABLE " + tableName + " ADD v 
VECTOR(DOUBLE, 64)");
+
+      PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+      PTable pTable = pconn.getTable(new PTableKey(null, tableName));
+      PColumn pColumn = pTable.getColumnForColumnName("V");
+      assertNotNull(pColumn);
+      assertEquals(PVectorDouble.INSTANCE, pColumn.getDataType());
+      assertEquals(Integer.valueOf(64), pColumn.getMaxLength());
+    }
+  }
+
+  @Test
+  public void testVectorColumnAsPrimaryKeyRejected() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      try {
+        conn.createStatement()
+          .execute("CREATE TABLE " + tableName + " (v VECTOR(FLOAT, 128) 
PRIMARY KEY)");
+        fail("Should have rejected vector column as primary key");
+      } catch (SQLException e) {
+        
assertEquals(SQLExceptionCode.INVALID_PRIMARY_KEY_CONSTRAINT.getErrorCode(),
+          e.getErrorCode());
+      }
+    }
+  }
+
+  @Test
+  public void testIntegerArrayConstructorAndCrossWidthBinds() throws Exception 
{
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement().execute("CREATE TABLE " + tableName
+        + " (pk INTEGER PRIMARY KEY, vf VECTOR(FLOAT, 3), vd VECTOR(DOUBLE, 
3))");
+      // An integer array literal coerces to a vector.
+      conn.createStatement()
+        .execute("UPSERT INTO " + tableName + " VALUES (1, ARRAY[1, 0, -2], 
ARRAY[3, 4, 5])");
+      // A numeric array of the other precision coerces to a vector.
+      try (PreparedStatement ps =
+        conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?, ?, 
?)")) {
+        ps.setInt(1, 2);
+        ps.setArray(2, conn.createArrayOf("DOUBLE", new Double[] { 1.5, 2.5, 
3.5 }));
+        ps.setArray(3, conn.createArrayOf("FLOAT", new Float[] { 4.5f, 5.5f, 
6.5f }));
+        ps.executeUpdate();
+      }
+      conn.commit();
+
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT vf, vd FROM " + tableName 
+ " ORDER BY pk")) {
+        assertTrue(rs.next());
+        assertArrayEquals(new float[] { 1f, 0f, -2f }, (float[]) 
rs.getObject(1), 0f);
+        assertArrayEquals(new double[] { 3, 4, 5 }, (double[]) 
rs.getObject(2), 0);
+        assertTrue(rs.next());
+        assertArrayEquals(new float[] { 1.5f, 2.5f, 3.5f }, (float[]) 
rs.getObject(1), 0f);
+        assertArrayEquals(new double[] { 4.5, 5.5, 6.5 }, (double[]) 
rs.getObject(2), 0);
+      }
+    }
+  }
+
+  @Test
+  public void testNullElementRejected() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+      // A NULL element in an array literal causes a constraint violation.
+      try {
+        conn.createStatement()
+          .execute("UPSERT INTO " + tableName + " VALUES (1, ARRAY[1.0, NULL, 
3.0])");
+        fail("A NULL vector element must be rejected, not stored as zero");
+      } catch (SQLException e) {
+        assertTrue(e.getMessage(), e.getMessage().contains("must not be 
NULL"));
+      }
+    }
+  }
+
+  @Test
+  public void testComparisonRejected() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " (pk INTEGER PRIMARY KEY, v 
VECTOR(FLOAT, 3))");
+      try {
+        conn.createStatement()
+          .executeQuery("SELECT pk FROM " + tableName + " WHERE v = ARRAY[1.0, 
2.0, 3.0]");
+        fail("Vectors do not support comparison");
+      } catch (SQLException e) {
+        assertEquals(SQLExceptionCode.COMPARISON_UNSUPPORTED.getErrorCode(), 
e.getErrorCode());
+      }
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/VectorColumnCompileTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/VectorColumnCompileTest.java
new file mode 100644
index 0000000000..d68f2b7ed0
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/VectorColumnCompileTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.compile;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collections;
+import org.apache.phoenix.exception.SQLExceptionCode;
+import org.apache.phoenix.jdbc.PhoenixStatement;
+import org.apache.phoenix.query.BaseConnectionlessQueryTest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Tests the compilation and the write-time validation of vector column 
values. */
+public class VectorColumnCompileTest extends BaseConnectionlessQueryTest {
+
+  private static final int DIM = 1024;
+
+  @BeforeClass
+  public static void setupTables() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE T_VEC_COMPILE (ID VARCHAR NOT NULL PRIMARY KEY, 
V VECTOR(FLOAT, "
+          + DIM + "), VD VECTOR(DOUBLE, 3), A FLOAT ARRAY, S VARCHAR)");
+    }
+  }
+
+  /**
+   * No conversion from a vector to an array or to VARCHAR exists. Thus such 
casts and assignments
+   * must fail at compile time, not on each row at execution time.
+   */
+  @Test
+  public void testVectorToArrayOrVarcharFailsAtCompileTime() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      PhoenixStatement stmt = 
conn.createStatement().unwrap(PhoenixStatement.class);
+      for (String sql : new String[] { "SELECT CAST(V AS FLOAT ARRAY) FROM 
T_VEC_COMPILE",
+        "SELECT CAST(VD AS DOUBLE ARRAY) FROM T_VEC_COMPILE",
+        "SELECT CAST(V AS VARCHAR) FROM T_VEC_COMPILE" }) {
+        try {
+          stmt.compileQuery(sql);
+          fail("Expected type mismatch compiling " + sql);
+        } catch (SQLException e) {
+          assertEquals(sql, SQLExceptionCode.TYPE_MISMATCH.getErrorCode(), 
e.getErrorCode());
+        }
+      }
+      for (String sql : new String[] {
+        "UPSERT INTO T_VEC_COMPILE (ID, A) SELECT ID, V FROM T_VEC_COMPILE",
+        "UPSERT INTO T_VEC_COMPILE (ID, S) SELECT ID, V FROM T_VEC_COMPILE" }) 
{
+        try {
+          stmt.compileMutation(sql);
+          fail("Expected type mismatch compiling " + sql);
+        } catch (SQLException e) {
+          assertEquals(sql, SQLExceptionCode.TYPE_MISMATCH.getErrorCode(), 
e.getErrorCode());
+        }
+      }
+      // The conversion from an array to a vector stays valid.
+      stmt.compileMutation("UPSERT INTO T_VEC_COMPILE (ID, V) SELECT ID, A 
FROM T_VEC_COMPILE");
+    }
+  }
+
+  /** The size of a vector is the dimension times the element width in bytes, 
not the dimension. */
+  @Test
+  public void testEstimatedRowSizeCountsVectorBytes() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      PhoenixStatement stmt = 
conn.createStatement().unwrap(PhoenixStatement.class);
+      for (String sql : new String[] { "SELECT V FROM T_VEC_COMPILE",
+        "SELECT * FROM T_VEC_COMPILE" }) {
+        int estimate = 
stmt.optimizeQuery(sql).getProjector().getEstimatedRowByteSize();
+        assertTrue(sql + " estimated " + estimate, estimate >= DIM * 
Float.BYTES);
+      }
+    }
+  }
+
+  /** A NaN or infinite element fails the write, from a bound value or from an 
array coercion. */
+  @Test
+  public void testNonFiniteVectorElementsRejectedOnUpsert() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      try (PreparedStatement ps =
+        conn.prepareStatement("UPSERT INTO T_VEC_COMPILE (ID, VD) VALUES ('a', 
?)")) {
+        ps.setArray(1, conn.createArrayOf("DOUBLE", new Double[] { 1.0, 
Double.NaN, 0.0 }));
+        assertConstraintViolation(ps::execute);
+        ps.setArray(1, conn.createArrayOf("DOUBLE", new Double[] { 1.0, 2.0, 
3.0 }));
+        ps.execute();
+      }
+      // 1e39 is a finite DOUBLE, but it overflows FLOAT to infinity.
+      String values = "0, 1e39" + String.join("", Collections.nCopies(DIM - 2, 
", 0"));
+      assertConstraintViolation(() -> conn.createStatement()
+        .execute("UPSERT INTO T_VEC_COMPILE (ID, V) VALUES ('b', ARRAY[" + 
values + "])"));
+      conn.createStatement().execute("UPSERT INTO T_VEC_COMPILE (ID, V) VALUES 
('b', ARRAY["
+        + values.replace("1e39", "1e38") + "])");
+    }
+  }
+
+  private interface SqlAction {
+    void run() throws SQLException;
+  }
+
+  private static void assertConstraintViolation(SqlAction action) {
+    try {
+      action.run();
+      fail("Expected a constraint violation for a non-finite vector element");
+    } catch (SQLException e) {
+      assertEquals(SQLExceptionCode.CONSTRAINT_VIOLATION.getErrorCode(), 
e.getErrorCode());
+      assertTrue(e.getMessage(), e.getMessage().contains("must be finite"));
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/CoerceExpressionTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/CoerceExpressionTest.java
index a6ab6fa588..71ae9b595c 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/CoerceExpressionTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/CoerceExpressionTest.java
@@ -59,6 +59,8 @@ public class CoerceExpressionTest {
     map.put(Date.class, new Date(0));
     map.put(Boolean.class, Boolean.TRUE);
     map.put(byte[].class, new byte[] { -128, 0, 0, 1 });
+    map.put(float[].class, new float[] { 1.00f, 2.00f });
+    map.put(double[].class, new double[] { 1.00d, 2.00d });
   }
 
   @Test
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/parse/VectorColumnParseTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/VectorColumnParseTest.java
new file mode 100644
index 0000000000..3eb8dc97f3
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/VectorColumnParseTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.apache.phoenix.exception.SQLExceptionCode;
+import org.apache.phoenix.schema.SortOrder;
+import org.apache.phoenix.schema.types.PVectorDouble;
+import org.apache.phoenix.schema.types.PVectorFloat;
+import org.junit.Test;
+
+/** Tests the parse of VECTOR column definitions in SQL statements. */
+public class VectorColumnParseTest {
+
+  @Test
+  public void testParseVectorFloatColumn() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(FLOAT, 
128))";
+    SQLParser parser = new SQLParser(ddl);
+    BindableStatement stmt = parser.parseStatement();
+    assertTrue("Expected CreateTableStatement", stmt instanceof 
CreateTableStatement);
+    CreateTableStatement createStmt = (CreateTableStatement) stmt;
+
+    List<ColumnDef> colDefs = createStmt.getColumnDefs();
+    assertEquals(2, colDefs.size());
+
+    ColumnDef vectorCol = colDefs.get(1);
+    assertEquals("V", vectorCol.getColumnDefName().getColumnName());
+    assertEquals(PVectorFloat.INSTANCE, vectorCol.getDataType());
+    assertEquals(Integer.valueOf(128), vectorCol.getMaxLength());
+  }
+
+  @Test
+  public void testParseVectorDoubleColumn() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(DOUBLE, 
64))";
+    SQLParser parser = new SQLParser(ddl);
+    BindableStatement stmt = parser.parseStatement();
+    assertTrue("Expected CreateTableStatement", stmt instanceof 
CreateTableStatement);
+    CreateTableStatement createStmt = (CreateTableStatement) stmt;
+
+    List<ColumnDef> colDefs = createStmt.getColumnDefs();
+    assertEquals(2, colDefs.size());
+
+    ColumnDef vectorCol = colDefs.get(1);
+    assertEquals("V", vectorCol.getColumnDefName().getColumnName());
+    assertEquals(PVectorDouble.INSTANCE, vectorCol.getDataType());
+    assertEquals(Integer.valueOf(64), vectorCol.getMaxLength());
+  }
+
+  @Test
+  public void testParseVectorColumnWithNotNull() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(FLOAT, 256) 
NOT NULL)";
+    SQLParser parser = new SQLParser(ddl);
+    CreateTableStatement createStmt = (CreateTableStatement) 
parser.parseStatement();
+
+    ColumnDef vectorCol = createStmt.getColumnDefs().get(1);
+    assertEquals(PVectorFloat.INSTANCE, vectorCol.getDataType());
+    assertEquals(Integer.valueOf(256), vectorCol.getMaxLength());
+    assertFalse("Column should be NOT NULL", vectorCol.isNull());
+  }
+
+  @Test
+  public void testZeroDimensionRejection() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(FLOAT, 0))";
+    try {
+      SQLParser parser = new SQLParser(ddl);
+      parser.parseStatement();
+      fail("Should have rejected zero dimension vector column definition");
+    } catch (SQLException e) {
+      assertEquals("Expected NONPOSITIVE_MAX_LENGTH error code",
+        SQLExceptionCode.NONPOSITIVE_MAX_LENGTH.getErrorCode(), 
e.getErrorCode());
+    }
+  }
+
+  @Test
+  public void testMissingDimensionRejection() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(FLOAT))";
+    try {
+      SQLParser parser = new SQLParser(ddl);
+      parser.parseStatement();
+      fail("Should have rejected missing dimension vector column definition");
+    } catch (SQLException e) {
+      // The parser rejects a vector column definition without a dimension.
+      assertNotNull("Expected parse exception", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testNegativeDimensionRejection() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(FLOAT, 
-5))";
+    try {
+      SQLParser parser = new SQLParser(ddl);
+      parser.parseStatement();
+      fail("Should have rejected negative dimension vector column definition");
+    } catch (SQLException e) {
+      assertNotNull("Expected parse exception for negative dimension", 
e.getMessage());
+    }
+  }
+
+  @Test
+  public void testInvalidComponentTypeRejection() throws Exception {
+    String ddl = "CREATE TABLE t (pk INTEGER PRIMARY KEY, v VECTOR(INT, 128))";
+    try {
+      SQLParser parser = new SQLParser(ddl);
+      parser.parseStatement();
+      fail("Should have rejected unsupported component type in vector column 
definition");
+    } catch (SQLException e) {
+      assertNotNull("Expected parse exception for invalid component type", 
e.getMessage());
+    }
+  }
+
+  @Test
+  public void testDynamicVectorColumn() throws Exception {
+    String sql = "SELECT * FROM t(v VECTOR(FLOAT, 128))";
+    SQLParser parser = new SQLParser(sql);
+    SelectStatement select = (SelectStatement) parser.parseStatement();
+    NamedTableNode tableNode = (NamedTableNode) select.getFrom();
+    List<ColumnDef> dynCols = tableNode.getDynamicColumns();
+    assertEquals(1, dynCols.size());
+    ColumnDef dynCol = dynCols.get(0);
+    assertEquals("V", dynCol.getColumnDefName().getColumnName());
+    assertEquals(PVectorFloat.INSTANCE, dynCol.getDataType());
+    assertEquals(Integer.valueOf(128), dynCol.getMaxLength());
+  }
+
+  @Test
+  public void testColumnDefConstructorValidation() {
+    ColumnName colName = new ColumnName("V");
+    try {
+      new ColumnDef(colName, PVectorFloat.INSTANCE.getSqlTypeName(), null, 
null, null, false,
+        SortOrder.getDefault(), null, null, false);
+      fail("Should reject null dimension for vector type");
+    } catch (ParseException e) {
+      assertTrue("Cause must be SQLException", e.getCause() instanceof 
SQLException);
+      assertEquals(SQLExceptionCode.MISSING_MAX_LENGTH.getErrorCode(),
+        ((SQLException) e.getCause()).getErrorCode());
+    }
+
+    try {
+      new ColumnDef(colName, PVectorFloat.INSTANCE.getSqlTypeName(), null, 0, 
null, false,
+        SortOrder.getDefault(), null, null, false);
+      fail("Should reject zero dimension for vector type");
+    } catch (ParseException e) {
+      assertTrue("Cause must be SQLException", e.getCause() instanceof 
SQLException);
+      assertEquals(SQLExceptionCode.NONPOSITIVE_MAX_LENGTH.getErrorCode(),
+        ((SQLException) e.getCause()).getErrorCode());
+    }
+
+    try {
+      new ColumnDef(colName, PVectorDouble.INSTANCE.getSqlTypeName(), null, 
-1, null, false,
+        SortOrder.getDefault(), null, null, false);
+      fail("Should reject negative dimension for vector type");
+    } catch (ParseException e) {
+      assertTrue("Cause must be SQLException", e.getCause() instanceof 
SQLException);
+      assertEquals(SQLExceptionCode.NONPOSITIVE_MAX_LENGTH.getErrorCode(),
+        ((SQLException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testColumnDefToString() {
+    ColumnDef floatVec = new ColumnDef(new ColumnName("V"), 
PVectorFloat.INSTANCE.getSqlTypeName(),
+      null, 128, null, false, SortOrder.getDefault(), null, null, false);
+    assertEquals("V VECTOR(FLOAT, 128)", floatVec.toString());
+
+    ColumnDef doubleVec =
+      new ColumnDef(new ColumnName("V"), 
PVectorDouble.INSTANCE.getSqlTypeName(), null, 64, null,
+        false, SortOrder.getDefault(), null, null, false);
+    assertEquals("V VECTOR(DOUBLE, 64)", doubleVec.toString());
+  }
+
+  @Test
+  public void testVectorIsNotReserved() throws Exception {
+    // VECTOR is not a reserved word. It is valid as a table, column, family, 
or alias name.
+    CreateTableStatement create = (CreateTableStatement) new SQLParser(
+      "CREATE TABLE vector (pk INTEGER PRIMARY KEY, vector VARBINARY, f.vector 
INTEGER)")
+        .parseStatement();
+    assertEquals("VECTOR", create.getTableName().getTableName());
+    assertEquals("VECTOR", 
create.getColumnDefs().get(1).getColumnDefName().getColumnName());
+    assertEquals("VARBINARY", 
create.getColumnDefs().get(1).getDataType().getSqlTypeName());
+    assertEquals("VECTOR", 
create.getColumnDefs().get(2).getColumnDefName().getColumnName());
+
+    SelectStatement select = (SelectStatement) new SQLParser(
+      "SELECT vector, v AS vector FROM vector WHERE vector IS NOT NULL ORDER 
BY vector")
+        .parseStatement();
+    assertEquals("VECTOR", select.getSelect().get(1).getAlias());
+
+    // A column with the name VECTOR can have a vector data type.
+    create = (CreateTableStatement) new SQLParser(
+      "CREATE TABLE t (pk INTEGER PRIMARY KEY, vector VECTOR(FLOAT, 
3))").parseStatement();
+    assertEquals("VECTOR", 
create.getColumnDefs().get(1).getColumnDefName().getColumnName());
+    assertEquals(PVectorFloat.INSTANCE, 
create.getColumnDefs().get(1).getDataType());
+    assertEquals(Integer.valueOf(3), 
create.getColumnDefs().get(1).getMaxLength());
+
+    // A dynamic column with the name VECTOR can have a non-vector data type.
+    select = (SelectStatement) new SQLParser("SELECT * FROM t(vector 
VARCHAR)").parseStatement();
+    ColumnDef dynCol = ((NamedTableNode) 
select.getFrom()).getDynamicColumns().get(0);
+    assertEquals("VECTOR", dynCol.getColumnDefName().getColumnName());
+    assertEquals("VARCHAR", dynCol.getDataType().getSqlTypeName());
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/PDataTypeTest.java 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/PDataTypeTest.java
index 40009dd8fc..805a9a3d55 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/PDataTypeTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/PDataTypeTest.java
@@ -1779,7 +1779,7 @@ public class PDataTypeTest {
       }
     }
     assertEquals("{BIGINT=[BIGINT, BINARY, DECIMAL, DOUBLE, VARBINARY], "
-      + "BIGINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, VARBINARY ARRAY], "
+      + "BIGINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "BINARY=[BINARY, VARBINARY, VARBINARY_ENCODED], "
       + "BINARY ARRAY=[BINARY ARRAY, VARBINARY ARRAY], "
       + "BOOLEAN=[BINARY, BOOLEAN, VARBINARY, VARBINARY_ENCODED], "
@@ -1789,46 +1789,47 @@ public class PDataTypeTest {
       + "DATE=[BINARY, DATE, TIME, TIMESTAMP, VARBINARY], "
       + "DATE ARRAY=[BINARY ARRAY, DATE ARRAY, TIME ARRAY, TIMESTAMP ARRAY, 
VARBINARY ARRAY], "
       + "DECIMAL=[DECIMAL, VARBINARY, VARBINARY_ENCODED], "
-      + "DECIMAL ARRAY=[DECIMAL ARRAY, VARBINARY ARRAY], "
+      + "DECIMAL ARRAY=[DECIMAL ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), 
VECTOR(FLOAT)], "
       + "DOUBLE=[BINARY, DECIMAL, DOUBLE, VARBINARY], "
-      + "DOUBLE ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, VARBINARY 
ARRAY], "
+      + "DOUBLE ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, VARBINARY 
ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "FLOAT=[BINARY, DECIMAL, DOUBLE, FLOAT, VARBINARY], "
-      + "FLOAT ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, FLOAT ARRAY, 
VARBINARY ARRAY], "
+      + "FLOAT ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, FLOAT ARRAY, 
VARBINARY ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "INTEGER=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, VARBINARY], 
"
-      + "INTEGER ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, VARBINARY ARRAY], "
+      + "INTEGER ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), 
VECTOR(FLOAT)], "
       + "JSON=[BINARY, JSON, VARBINARY], "
       + "SMALLINT=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, SMALLINT, 
VARBINARY], "
-      + "SMALLINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, VARBINARY ARRAY], "
+      + "SMALLINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, VARBINARY ARRAY, 
VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "TIME=[BINARY, DATE, TIME, TIMESTAMP, VARBINARY], "
       + "TIME ARRAY=[BINARY ARRAY, DATE ARRAY, TIME ARRAY, TIMESTAMP ARRAY, 
VARBINARY ARRAY], "
       + "TIMESTAMP=[BINARY, TIMESTAMP, VARBINARY], "
       + "TIMESTAMP ARRAY=[BINARY ARRAY, TIMESTAMP ARRAY, VARBINARY ARRAY], "
       + "TINYINT=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, SMALLINT, 
TINYINT, VARBINARY], "
-      + "TINYINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, TINYINT ARRAY, VARBINARY 
ARRAY], "
+      + "TINYINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, TINYINT ARRAY, VARBINARY 
ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "UNSIGNED_DATE=[BINARY, DATE, TIME, TIMESTAMP, UNSIGNED_DATE, 
UNSIGNED_TIME, UNSIGNED_TIMESTAMP, VARBINARY], "
       + "UNSIGNED_DATE ARRAY=[BINARY ARRAY, DATE ARRAY, TIME ARRAY, TIMESTAMP 
ARRAY, UNSIGNED_DATE ARRAY, UNSIGNED_TIME ARRAY, UNSIGNED_TIMESTAMP ARRAY, 
VARBINARY ARRAY], "
       + "UNSIGNED_DOUBLE=[BINARY, DECIMAL, DOUBLE, UNSIGNED_DOUBLE, 
VARBINARY], "
-      + "UNSIGNED_DOUBLE ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, 
UNSIGNED_DOUBLE ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_DOUBLE ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, 
UNSIGNED_DOUBLE ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "UNSIGNED_FLOAT=[BINARY, DECIMAL, DOUBLE, FLOAT, UNSIGNED_DOUBLE, 
UNSIGNED_FLOAT, VARBINARY], "
-      + "UNSIGNED_FLOAT ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, 
FLOAT ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_FLOAT ARRAY=[BINARY ARRAY, DECIMAL ARRAY, DOUBLE ARRAY, 
FLOAT ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, VARBINARY ARRAY, 
VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "UNSIGNED_INT=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, 
UNSIGNED_DOUBLE, UNSIGNED_FLOAT, UNSIGNED_INT, UNSIGNED_LONG, VARBINARY], "
-      + "UNSIGNED_INT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, 
UNSIGNED_INT ARRAY, UNSIGNED_LONG ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_INT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, DOUBLE 
ARRAY, FLOAT ARRAY, INTEGER ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, 
UNSIGNED_INT ARRAY, UNSIGNED_LONG ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), 
VECTOR(FLOAT)], "
       + "UNSIGNED_LONG=[BIGINT, BINARY, DECIMAL, DOUBLE, UNSIGNED_DOUBLE, 
UNSIGNED_LONG, VARBINARY], "
-      + "UNSIGNED_LONG ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_LONG ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_LONG ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, UNSIGNED_DOUBLE ARRAY, UNSIGNED_LONG ARRAY, VARBINARY ARRAY, 
VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "UNSIGNED_SMALLINT=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, 
SMALLINT, UNSIGNED_DOUBLE, UNSIGNED_FLOAT, UNSIGNED_INT, UNSIGNED_LONG, 
UNSIGNED_SMALLINT, VARBINARY], "
-      + "UNSIGNED_SMALLINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, UNSIGNED_DOUBLE 
ARRAY, UNSIGNED_FLOAT ARRAY, UNSIGNED_INT ARRAY, UNSIGNED_LONG ARRAY, 
UNSIGNED_SMALLINT ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_SMALLINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, UNSIGNED_DOUBLE 
ARRAY, UNSIGNED_FLOAT ARRAY, UNSIGNED_INT ARRAY, UNSIGNED_LONG ARRAY, 
UNSIGNED_SMALLINT ARRAY, VARBINARY ARRAY, VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "UNSIGNED_TIME=[BINARY, DATE, TIME, TIMESTAMP, UNSIGNED_DATE, 
UNSIGNED_TIME, UNSIGNED_TIMESTAMP, VARBINARY], "
       + "UNSIGNED_TIME ARRAY=[BINARY ARRAY, DATE ARRAY, TIME ARRAY, TIMESTAMP 
ARRAY, UNSIGNED_DATE ARRAY, UNSIGNED_TIME ARRAY, UNSIGNED_TIMESTAMP ARRAY, 
VARBINARY ARRAY], "
       + "UNSIGNED_TIMESTAMP=[BINARY, DATE, TIME, TIMESTAMP, UNSIGNED_DATE, 
UNSIGNED_TIME, UNSIGNED_TIMESTAMP, VARBINARY], "
       + "UNSIGNED_TIMESTAMP ARRAY=[BINARY ARRAY, DATE ARRAY, TIME ARRAY, 
TIMESTAMP ARRAY, UNSIGNED_DATE ARRAY, UNSIGNED_TIME ARRAY, UNSIGNED_TIMESTAMP 
ARRAY, VARBINARY ARRAY], "
       + "UNSIGNED_TINYINT=[BIGINT, BINARY, DECIMAL, DOUBLE, FLOAT, INTEGER, 
SMALLINT, TINYINT, UNSIGNED_DOUBLE, UNSIGNED_FLOAT, UNSIGNED_INT, 
UNSIGNED_LONG, UNSIGNED_SMALLINT, UNSIGNED_TINYINT, VARBINARY], "
-      + "UNSIGNED_TINYINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, TINYINT ARRAY, 
UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, UNSIGNED_INT ARRAY, UNSIGNED_LONG 
ARRAY, UNSIGNED_SMALLINT ARRAY, UNSIGNED_TINYINT ARRAY, VARBINARY ARRAY], "
+      + "UNSIGNED_TINYINT ARRAY=[BIGINT ARRAY, BINARY ARRAY, DECIMAL ARRAY, 
DOUBLE ARRAY, FLOAT ARRAY, INTEGER ARRAY, SMALLINT ARRAY, TINYINT ARRAY, 
UNSIGNED_DOUBLE ARRAY, UNSIGNED_FLOAT ARRAY, UNSIGNED_INT ARRAY, UNSIGNED_LONG 
ARRAY, UNSIGNED_SMALLINT ARRAY, UNSIGNED_TINYINT ARRAY, VARBINARY ARRAY, 
VECTOR(DOUBLE), VECTOR(FLOAT)], "
       + "VARBINARY=[BINARY, VARBINARY, VARBINARY_ENCODED], "
       + "VARBINARY ARRAY=[BINARY ARRAY, VARBINARY ARRAY], "
       + "VARBINARY_ENCODED=[BINARY, VARBINARY, VARBINARY_ENCODED], "
       + "VARCHAR=[BINARY, BSON, CHAR, VARBINARY, VARBINARY_ENCODED, VARCHAR], "
-      + "VARCHAR ARRAY=[BINARY ARRAY, CHAR ARRAY, VARBINARY ARRAY, VARCHAR 
ARRAY]}",
-      coercibleToMap.toString());
+      + "VARCHAR ARRAY=[BINARY ARRAY, CHAR ARRAY, VARBINARY ARRAY, VARCHAR 
ARRAY], "
+      + "VECTOR(DOUBLE)=[VARBINARY, VARBINARY_ENCODED, VECTOR(DOUBLE)], "
+      + "VECTOR(FLOAT)=[VARBINARY, VARBINARY_ENCODED, VECTOR(FLOAT)]}", 
coercibleToMap.toString());
   }
 
   @Test
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
new file mode 100644
index 0000000000..e1cd9ed490
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
@@ -0,0 +1,559 @@
+/*
+ * 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.schema.types;
+
+import static org.junit.Assert.assertArrayEquals;
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.coprocessor.generated.PTableProtos;
+import org.apache.phoenix.schema.ConstraintViolationException;
+import org.apache.phoenix.schema.PColumn;
+import org.apache.phoenix.schema.PColumnImpl;
+import org.apache.phoenix.schema.PNameFactory;
+import org.apache.phoenix.schema.SortOrder;
+import org.junit.Test;
+
+/** Tests the vector data types, type factory resolution, column metadata, and 
serialization. */
+public class VectorDataTypeTest {
+
+  @Test
+  public void testFactoryTypeForSqlType() {
+    PDataTypeFactory factory = PDataTypeFactory.getInstance();
+    assertSame(PVectorFloat.INSTANCE, 
PDataType.fromTypeId(PDataType.VECTOR_FLOAT_TYPE));
+    assertSame(PVectorDouble.INSTANCE, 
PDataType.fromTypeId(PDataType.VECTOR_DOUBLE_TYPE));
+    assertEquals(4001, PVectorFloat.INSTANCE.getSqlType());
+    assertEquals(4002, PVectorDouble.INSTANCE.getSqlType());
+  }
+
+  @Test
+  public void testFactoryTypeEnumeration() {
+    PDataTypeFactory factory = PDataTypeFactory.getInstance();
+    assertTrue(factory.getTypes().contains(PVectorFloat.INSTANCE));
+    assertTrue(factory.getTypes().contains(PVectorDouble.INSTANCE));
+  }
+
+  @Test
+  public void testSqlTypeNameResolution() {
+    PDataTypeFactory factory = PDataTypeFactory.getInstance();
+    assertSame(PVectorFloat.INSTANCE, 
PDataType.fromSqlTypeName("VECTOR(FLOAT)"));
+    assertSame(PVectorFloat.INSTANCE, 
PDataType.fromSqlTypeName("vector(float)"));
+    assertSame(PVectorDouble.INSTANCE, 
PDataType.fromSqlTypeName("VECTOR(DOUBLE)"));
+    assertSame(PVectorFloat.INSTANCE, factory.typeForVector("float"));
+    assertSame(PVectorDouble.INSTANCE, factory.typeForVector("DOUBLE"));
+    assertNull(factory.typeForVector("INTEGER"));
+  }
+
+  @Test
+  public void testVectorFloatProperties() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    assertTrue(type.isVectorType());
+    assertTrue(type.isFixedWidth());
+    assertNull(type.getByteSize());
+    assertFalse(type.isComparisonSupported());
+
+    assertEquals(Integer.valueOf(12), type.estimateByteSizeFromLength(3));
+    // Arrays coerce to vectors. No conversion from a vector to an array or 
VARCHAR exists.
+    assertTrue(PFloatArray.INSTANCE.isCoercibleTo(type));
+    assertFalse(type.isCoercibleTo(PFloatArray.INSTANCE));
+    assertFalse(type.isCoercibleTo(PVarchar.INSTANCE));
+    assertFalse(type.isCastableTo(PFloatArray.INSTANCE));
+    assertFalse(type.isCastableTo(PVarchar.INSTANCE));
+    assertTrue(type.isCastableTo(type));
+    assertTrue(type.isCastableTo(PVarbinary.INSTANCE));
+  }
+
+  @Test
+  public void testVectorFloatSerializationRoundTrip() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    float[] original = new float[] { 1.0f, -2.5f, 3.14159f };
+    byte[] bytes = type.toBytes(original);
+    assertEquals(original.length * Bytes.SIZEOF_FLOAT, bytes.length);
+
+    float[] deserialized = (float[]) type.toObject(bytes);
+    assertArrayEquals(original, deserialized, 0.00001f);
+
+    byte[] bytesDesc = type.toBytes(original, SortOrder.DESC);
+    float[] deserializedDesc = (float[]) type.toObject(bytesDesc, 0, 
bytesDesc.length, type,
+      SortOrder.DESC, original.length, null);
+    assertArrayEquals(original, deserializedDesc, 0.00001f);
+  }
+
+  @Test
+  public void testVectorFloatCompareTo() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    float[] a = new float[] { 1.0f, 2.0f, 3.0f };
+    float[] b = new float[] { 1.0f, 2.0f, 3.0f };
+    float[] c = new float[] { 1.0f, 2.0f, 4.0f };
+    float[] d = new float[] { 1.0f, 2.0f };
+
+    assertEquals(0, type.compareTo(a, b, type));
+    assertTrue(type.compareTo(a, c, type) < 0);
+    assertTrue(type.compareTo(c, a, type) > 0);
+    assertTrue(type.compareTo(a, d, type) > 0);
+    assertTrue(type.compareTo(d, a, type) < 0);
+  }
+
+  @Test
+  public void testVectorFloatCoerceBytes() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    float[] original = new float[] { 1.0f, 2.0f, 3.0f };
+    byte[] ascBytes = type.toBytes(original, SortOrder.ASC);
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable(ascBytes);
+
+    type.coerceBytes(ptr, null, type, 3, null, SortOrder.ASC, 3, null, 
SortOrder.DESC);
+    float[] deserialized = (float[]) type.toObject(ptr.get(), ptr.getOffset(), 
ptr.getLength(),
+      type, SortOrder.DESC, 3, null);
+    assertArrayEquals(original, deserialized, 0.00001f);
+
+    try {
+      type.coerceBytes(ptr, null, type, 3, null, SortOrder.DESC, 4, null, 
SortOrder.ASC);
+      fail("Should have thrown ConstraintViolationException on dimension 
mismatch");
+    } catch (ConstraintViolationException expected) {
+    }
+  }
+
+  @Test
+  public void testVectorFloatIsSizeCompatible() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    float[] vector = new float[] { 1.0f, 2.0f, 3.0f };
+    byte[] bytes = type.toBytes(vector);
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable(bytes);
+
+    assertTrue(type.isSizeCompatible(ptr, vector, type, SortOrder.ASC, 3, 
null, 3, null));
+    try {
+      type.isSizeCompatible(ptr, vector, type, SortOrder.ASC, 3, null, 4, 
null);
+      fail("Should have thrown ConstraintViolationException on dimension 
mismatch");
+    } catch (ConstraintViolationException expected) {
+    }
+  }
+
+  @Test
+  public void testVectorFloatStringConversion() {
+    PVectorFloat type = PVectorFloat.INSTANCE;
+    float[] vector = new float[] { 1.0f, 2.5f, -3.0f };
+    assertEquals("[1.0, 2.5, -3.0]", type.toStringLiteral(vector, null));
+
+    float[] parsed1 = (float[]) type.toObject("[1.0, 2.5, -3.0]");
+    assertArrayEquals(vector, parsed1, 0.00001f);
+
+    float[] parsed2 = (float[]) type.toObject("1.0, 2.5, -3.0");
+    assertArrayEquals(vector, parsed2, 0.00001f);
+
+    float[] parsedEmpty = (float[]) type.toObject("[]");
+    assertEquals(0, parsedEmpty.length);
+  }
+
+  @Test
+  public void testVectorDoubleProperties() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    assertTrue(type.isVectorType());
+    assertTrue(type.isFixedWidth());
+    assertNull(type.getByteSize());
+    assertEquals(Integer.valueOf(24), type.estimateByteSizeFromLength(3));
+    assertFalse(type.isComparisonSupported());
+
+    assertEquals(Integer.valueOf(24), type.estimateByteSizeFromLength(3));
+    // Arrays coerce to vectors. No conversion from a vector to an array or 
VARCHAR exists.
+    assertTrue(PDoubleArray.INSTANCE.isCoercibleTo(type));
+    assertFalse(type.isCoercibleTo(PDoubleArray.INSTANCE));
+    assertFalse(type.isCoercibleTo(PVarchar.INSTANCE));
+    assertFalse(type.isCastableTo(PDoubleArray.INSTANCE));
+    assertFalse(type.isCastableTo(PVarchar.INSTANCE));
+    assertTrue(type.isCastableTo(type));
+    assertTrue(type.isCastableTo(PVarbinary.INSTANCE));
+  }
+
+  @Test
+  public void testCanBePrimaryKeyReturnsFalse() {
+    assertFalse(PVectorFloat.INSTANCE.canBePrimaryKey());
+    assertFalse(PVectorDouble.INSTANCE.canBePrimaryKey());
+  }
+
+  @Test
+  public void testVectorDoubleSerializationRoundTrip() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    double[] original = new double[] { 1.0, -2.5, 3.141592653589793 };
+    byte[] bytes = type.toBytes(original);
+    assertEquals(original.length * Bytes.SIZEOF_DOUBLE, bytes.length);
+
+    double[] deserialized = (double[]) type.toObject(bytes);
+    assertArrayEquals(original, deserialized, 0.0000001);
+
+    byte[] bytesDesc = type.toBytes(original, SortOrder.DESC);
+    double[] deserializedDesc = (double[]) type.toObject(bytesDesc, 0, 
bytesDesc.length, type,
+      SortOrder.DESC, original.length, null);
+    assertArrayEquals(original, deserializedDesc, 0.0000001);
+  }
+
+  @Test
+  public void testVectorDoubleCompareTo() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    double[] a = new double[] { 1.0, 2.0, 3.0 };
+    double[] b = new double[] { 1.0, 2.0, 3.0 };
+    double[] c = new double[] { 1.0, 2.0, 4.0 };
+    double[] d = new double[] { 1.0, 2.0 };
+
+    assertEquals(0, type.compareTo(a, b, type));
+    assertTrue(type.compareTo(a, c, type) < 0);
+    assertTrue(type.compareTo(c, a, type) > 0);
+    assertTrue(type.compareTo(a, d, type) > 0);
+    assertTrue(type.compareTo(d, a, type) < 0);
+  }
+
+  @Test
+  public void testVectorDoubleCoerceBytes() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    double[] original = new double[] { 1.0, 2.0, 3.0 };
+    byte[] ascBytes = type.toBytes(original, SortOrder.ASC);
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable(ascBytes);
+
+    type.coerceBytes(ptr, null, type, 3, null, SortOrder.ASC, 3, null, 
SortOrder.DESC);
+    double[] deserialized = (double[]) type.toObject(ptr.get(), 
ptr.getOffset(), ptr.getLength(),
+      type, SortOrder.DESC, 3, null);
+    assertArrayEquals(original, deserialized, 0.0000001);
+
+    try {
+      type.coerceBytes(ptr, null, type, 3, null, SortOrder.DESC, 4, null, 
SortOrder.ASC);
+      fail("Should have thrown ConstraintViolationException on dimension 
mismatch");
+    } catch (ConstraintViolationException expected) {
+    }
+  }
+
+  @Test
+  public void testVectorDoubleIsSizeCompatible() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    double[] vector = new double[] { 1.0, 2.0, 3.0 };
+    byte[] bytes = type.toBytes(vector);
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable(bytes);
+
+    assertTrue(type.isSizeCompatible(ptr, vector, type, SortOrder.ASC, 3, 
null, 3, null));
+    try {
+      type.isSizeCompatible(ptr, vector, type, SortOrder.ASC, 3, null, 4, 
null);
+      fail("Should have thrown ConstraintViolationException on dimension 
mismatch");
+    } catch (ConstraintViolationException expected) {
+    }
+  }
+
+  @Test
+  public void testVectorDoubleStringConversion() {
+    PVectorDouble type = PVectorDouble.INSTANCE;
+    double[] vector = new double[] { 1.0, 2.5, -3.0 };
+    assertEquals("[1.0, 2.5, -3.0]", type.toStringLiteral(vector, null));
+
+    double[] parsed1 = (double[]) type.toObject("[1.0, 2.5, -3.0]");
+    assertArrayEquals(vector, parsed1, 0.0000001);
+
+    double[] parsed2 = (double[]) type.toObject("1.0, 2.5, -3.0");
+    assertArrayEquals(vector, parsed2, 0.0000001);
+
+    double[] parsedEmpty = (double[]) type.toObject("[]");
+    assertEquals(0, parsedEmpty.length);
+  }
+
+  @Test
+  public void testVectorFloatProtobufRoundTrip() {
+    PColumn column =
+      new PColumnImpl(PNameFactory.newName("V"), PNameFactory.newName("0"), 
PVectorFloat.INSTANCE,
+        128, null, true, 1, SortOrder.ASC, null, null, false, null, false, 
false, null, 0L);
+
+    assertEquals(PNameFactory.newName("V"), column.getName());
+    assertEquals(PNameFactory.newName("0"), column.getFamilyName());
+    assertEquals(PVectorFloat.INSTANCE, column.getDataType());
+    assertEquals(Integer.valueOf(128), column.getMaxLength());
+    assertEquals(1, column.getPosition());
+    assertTrue(column.isNullable());
+
+    PTableProtos.PColumn proto = PColumnImpl.toProto(column);
+    assertNotNull(proto);
+    assertEquals("VECTOR(FLOAT)", proto.getDataType());
+    assertEquals(128, proto.getMaxLength());
+
+    PColumn deserialized = PColumnImpl.createFromProto(proto);
+    assertNotNull(deserialized);
+    assertEquals(column.getName(), deserialized.getName());
+    assertEquals(column.getFamilyName(), deserialized.getFamilyName());
+    assertEquals(PVectorFloat.INSTANCE, deserialized.getDataType());
+    assertEquals(Integer.valueOf(128), deserialized.getMaxLength());
+    assertEquals(column.getPosition(), deserialized.getPosition());
+    assertEquals(column.isNullable(), deserialized.isNullable());
+  }
+
+  @Test
+  public void testVectorDoubleProtobufRoundTrip() {
+    PColumn column =
+      new PColumnImpl(PNameFactory.newName("VD"), PNameFactory.newName("0"), 
PVectorDouble.INSTANCE,
+        256, null, false, 2, SortOrder.ASC, null, null, false, null, false, 
false, null, 0L);
+
+    assertEquals(PNameFactory.newName("VD"), column.getName());
+    assertEquals(PNameFactory.newName("0"), column.getFamilyName());
+    assertEquals(PVectorDouble.INSTANCE, column.getDataType());
+    assertEquals(Integer.valueOf(256), column.getMaxLength());
+    assertEquals(2, column.getPosition());
+    assertFalse(column.isNullable());
+
+    PTableProtos.PColumn proto = PColumnImpl.toProto(column);
+    assertNotNull(proto);
+    assertEquals("VECTOR(DOUBLE)", proto.getDataType());
+    assertEquals(256, proto.getMaxLength());
+
+    PColumn deserialized = PColumnImpl.createFromProto(proto);
+    assertNotNull(deserialized);
+    assertEquals(column.getName(), deserialized.getName());
+    assertEquals(column.getFamilyName(), deserialized.getFamilyName());
+    assertEquals(PVectorDouble.INSTANCE, deserialized.getDataType());
+    assertEquals(Integer.valueOf(256), deserialized.getMaxLength());
+    assertEquals(column.getPosition(), deserialized.getPosition());
+    assertEquals(column.isNullable(), deserialized.isNullable());
+  }
+
+  @Test
+  public void testFloatNativeEncodingRoundTrip() {
+    float[] original = new float[] { 1.0f, -2.5f, Float.NaN, 
Float.POSITIVE_INFINITY, 0.0f };
+    byte[] encoded = new byte[original.length * Bytes.SIZEOF_FLOAT];
+    PVectorFloat.writeElements(original, encoded, 0);
+
+    for (int i = 0; i < original.length; i++) {
+      int rawBits = Bytes.toInt(encoded, i * Bytes.SIZEOF_FLOAT);
+      assertEquals("Element " + i + " bit pattern mismatch", 
Float.floatToRawIntBits(original[i]),
+        rawBits);
+    }
+
+    float[] decoded = (float[]) PVectorFloat.INSTANCE.toObject(encoded, 0, 
encoded.length,
+      PVectorFloat.INSTANCE, SortOrder.ASC, null, null);
+    assertArrayEquals(original, decoded, 0.0f);
+  }
+
+  @Test
+  public void testDoubleNativeEncodingRoundTrip() {
+    double[] original = new double[] { 1.0, -2.5, Double.NaN, 
Double.NEGATIVE_INFINITY, 0.0 };
+    byte[] encoded = new byte[original.length * Bytes.SIZEOF_DOUBLE];
+    PVectorDouble.writeElements(original, encoded, 0);
+
+    for (int i = 0; i < original.length; i++) {
+      long rawBits = Bytes.toLong(encoded, i * Bytes.SIZEOF_DOUBLE);
+      assertEquals("Element " + i + " bit pattern mismatch",
+        Double.doubleToRawLongBits(original[i]), rawBits);
+    }
+
+    double[] decoded = (double[]) PVectorDouble.INSTANCE.toObject(encoded, 0, 
encoded.length,
+      PVectorDouble.INSTANCE, SortOrder.ASC, null, null);
+    assertArrayEquals(original, decoded, 0.0);
+  }
+
+  @Test
+  public void testFloatReadElementASC() {
+    float[] vector = new float[] { 3.14f, -1.0f, 0.0f, Float.MAX_VALUE };
+    byte[] bytes = PVectorFloat.INSTANCE.toBytes(vector);
+
+    for (int i = 0; i < vector.length; i++) {
+      float read = PVectorFloat.readElement(bytes, 0, i);
+      assertEquals("readElement[" + i + "]", vector[i], read, 0.0f);
+    }
+  }
+
+  @Test
+  public void testFloatReadElementDESC() {
+    float[] vector = new float[] { 1.0f, 2.0f, 3.0f };
+    byte[] bytes = PVectorFloat.INSTANCE.toBytes(vector, SortOrder.DESC);
+
+    for (int i = 0; i < vector.length; i++) {
+      float read = PVectorFloat.readElement(bytes, 0, i, SortOrder.DESC);
+      assertEquals("readElement DESC[" + i + "]", vector[i], read, 1e-5f);
+    }
+  }
+
+  @Test
+  public void testDoubleReadElementASC() {
+    double[] vector = new double[] { Math.PI, -Math.E, 0.0, Double.MAX_VALUE };
+    byte[] bytes = PVectorDouble.INSTANCE.toBytes(vector);
+
+    for (int i = 0; i < vector.length; i++) {
+      double read = PVectorDouble.readElement(bytes, 0, i);
+      assertEquals("readElement[" + i + "]", vector[i], read, 0.0);
+    }
+  }
+
+  @Test
+  public void testFloatWriteElementsAndReadElements() {
+    float[] vector = new float[] { 10.0f, 20.0f, 30.0f, 40.0f };
+    byte[] buf = new byte[vector.length * Bytes.SIZEOF_FLOAT + 4]; // Extra 
bytes before the offset
+    int offset = 4;
+    PVectorFloat.writeElements(vector, buf, offset);
+
+    float[] decoded = PVectorFloat.readElements(buf, offset, vector.length * 
Bytes.SIZEOF_FLOAT);
+    assertArrayEquals(vector, decoded, 0.0f);
+  }
+
+  @Test
+  public void testDoubleWriteElementsAndReadElements() {
+    double[] vector = new double[] { 100.0, 200.0, 300.0 };
+    byte[] buf = new byte[vector.length * Bytes.SIZEOF_DOUBLE + 8];
+    int offset = 8;
+    PVectorDouble.writeElements(vector, buf, offset);
+
+    double[] decoded = PVectorDouble.readElements(buf, offset, vector.length * 
Bytes.SIZEOF_DOUBLE);
+    assertArrayEquals(vector, decoded, 0.0);
+  }
+
+  @Test
+  public void testFloatReadElementsDescRoundTrip() {
+    float[] original = new float[] { 1.0f, 2.0f, 3.0f };
+    byte[] descBytes = PVectorFloat.INSTANCE.toBytes(original, SortOrder.DESC);
+
+    float[] decoded = PVectorFloat.readElements(descBytes, 0, 
descBytes.length, SortOrder.DESC);
+    assertArrayEquals(original, decoded, 1e-5f);
+  }
+
+  @Test
+  public void testDoubleReadElementsDescRoundTrip() {
+    double[] original = new double[] { 1.0, 2.0, 3.0 };
+    byte[] descBytes = PVectorDouble.INSTANCE.toBytes(original, 
SortOrder.DESC);
+
+    double[] decoded = PVectorDouble.readElements(descBytes, 0, 
descBytes.length, SortOrder.DESC);
+    assertArrayEquals(original, decoded, 1e-7);
+  }
+
+  /**
+   * Conversion to a vector rejects NaN and infinite elements from a primitive 
or boxed array, a
+   * Phoenix array, or a string. This includes a finite DOUBLE element that 
overflows FLOAT.
+   */
+  @Test
+  public void testNonFiniteElementsRejected() {
+    assertNonFiniteRejected(PVectorFloat.INSTANCE, new float[] { 1.0f, 
Float.NaN, 0.0f });
+    assertNonFiniteRejected(PVectorFloat.INSTANCE, new float[] { 
Float.NEGATIVE_INFINITY });
+    assertNonFiniteRejected(PVectorFloat.INSTANCE, new Double[] { 0.0, 1e39 });
+    assertNonFiniteRejected(PVectorFloat.INSTANCE,
+      PArrayDataType.instantiatePhoenixArray(PDouble.INSTANCE, new Double[] { 
1.0, Double.NaN }));
+    assertNonFiniteRejected(PVectorFloat.INSTANCE, "[1.0, Infinity]");
+
+    assertNonFiniteRejected(PVectorDouble.INSTANCE, new double[] { 1.0, 
Double.NaN });
+    assertNonFiniteRejected(PVectorDouble.INSTANCE, new float[] { 
Float.POSITIVE_INFINITY });
+    assertNonFiniteRejected(PVectorDouble.INSTANCE, "[NaN]");
+
+    // Extreme finite values stay valid.
+    float[] extremes = { Float.MAX_VALUE, -Float.MAX_VALUE, Float.MIN_VALUE, 
-0.0f };
+    assertArrayEquals(extremes, (float[]) PVectorFloat.INSTANCE
+      .toObject(PVectorFloat.INSTANCE.toBytes(extremes), 0, 4 * 
Bytes.SIZEOF_FLOAT), 0.0f);
+  }
+
+  /**
+   * A stored vector renders and compares without element checks. Thus a 
non-finite element that did
+   * not go through the type, for example from a raw HBase write or a 
replicated write, does not
+   * make the vector unreadable.
+   */
+  @Test
+  public void testStoredNonFiniteVectorRendersAndCompares() {
+    byte[] floats = new byte[3 * Bytes.SIZEOF_FLOAT];
+    PVectorFloat.writeElements(new float[] { 1.0f, Float.NaN, 
Float.POSITIVE_INFINITY }, floats, 0);
+    Object storedFloats = PVectorFloat.INSTANCE.toObject(floats, 0, 
floats.length);
+    assertEquals("[1.0, NaN, Infinity]", 
PVectorFloat.INSTANCE.toStringLiteral(storedFloats, null));
+    assertEquals(0, PVectorFloat.INSTANCE.compareTo(storedFloats,
+      PVectorFloat.INSTANCE.toObject(floats, 0, floats.length), 
PVectorFloat.INSTANCE));
+
+    byte[] doubles = new byte[2 * Bytes.SIZEOF_DOUBLE];
+    PVectorDouble.writeElements(new double[] { Double.NEGATIVE_INFINITY, 2.0 
}, doubles, 0);
+    Object storedDoubles = PVectorDouble.INSTANCE.toObject(doubles, 0, 
doubles.length);
+    assertEquals("[-Infinity, 2.0]", 
PVectorDouble.INSTANCE.toStringLiteral(storedDoubles, null));
+    assertTrue(PVectorDouble.INSTANCE.compareTo(storedDoubles, new double[] { 
1.0, 2.0 },
+      PVectorDouble.INSTANCE) != 0);
+  }
+
+  private static void assertNonFiniteRejected(PVectorDataType<?> type, Object 
value) {
+    try {
+      type.toObject(value, PFloatArray.INSTANCE);
+      fail("Expected rejection of " + value + " as " + type);
+    } catch (ConstraintViolationException expected) {
+      assertTrue(expected.getMessage(), expected.getMessage().contains("must 
be finite"));
+    }
+    if (type.getJavaClass().isInstance(value)) {
+      try {
+        type.toBytes(value);
+        fail("Expected toBytes rejection of " + value + " as " + type);
+      } catch (ConstraintViolationException expected) {
+        // expected
+      }
+    }
+  }
+
+  @Test
+  public void testWriteElementsBufferTooSmall() {
+    float[] vector = new float[] { 1.0f, 2.0f, 3.0f };
+    byte[] tooSmall = new byte[8];
+    try {
+      PVectorFloat.writeElements(vector, tooSmall, 0);
+      fail("Should throw on buffer too small");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDoubleWriteElementsBufferTooSmall() {
+    double[] vector = new double[] { 1.0, 2.0 };
+    byte[] tooSmall = new byte[8];
+    try {
+      PVectorDouble.writeElements(vector, tooSmall, 0);
+      fail("Should throw on buffer too small");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNumericArraysOfAnyElementTypeConvert() throws Exception {
+    // Numeric arrays coerce to vectors across element types and widths.
+    PhoenixArray ints =
+      PArrayDataType.instantiatePhoenixArray(PInteger.INSTANCE, new Integer[] 
{ 1, 0, -2 });
+    assertArrayEquals(new float[] { 1f, 0f, -2f },
+      (float[]) PVectorFloat.INSTANCE.toObject(ints, PIntegerArray.INSTANCE), 
0f);
+    assertArrayEquals(new double[] { 1, 0, -2 },
+      (double[]) PVectorDouble.INSTANCE.toObject(ints, 
PIntegerArray.INSTANCE), 0);
+    assertArrayEquals(new float[] { 1.5f, 2.5f },
+      (float[]) PVectorFloat.INSTANCE.toObject(new double[] { 1.5, 2.5 }, 
PDoubleArray.INSTANCE),
+      0f);
+    assertArrayEquals(new double[] { 1.5, 2.5 },
+      (double[]) PVectorDouble.INSTANCE.toObject(new float[] { 1.5f, 2.5f }, 
PFloatArray.INSTANCE),
+      0);
+    assertArrayEquals(new float[] { 1f, 2f },
+      (float[]) PVectorFloat.INSTANCE.toObject(new Long[] { 1L, 2L }, 
PLongArray.INSTANCE), 0f);
+    assertEquals(Integer.valueOf(3), PVectorFloat.INSTANCE.getMaxLength(ints));
+    assertTrue(PIntegerArray.INSTANCE.isCoercibleTo(PVectorFloat.INSTANCE));
+    assertTrue(PDecimalArray.INSTANCE.isCoercibleTo(PVectorDouble.INSTANCE));
+    assertFalse(PVarcharArray.INSTANCE.isCoercibleTo(PVectorFloat.INSTANCE));
+  }
+
+  @Test
+  public void testNullElementsRejected() {
+    try {
+      PVectorFloat.INSTANCE.toObject(new Float[] { 1f, null, 3f }, 
PFloatArray.INSTANCE);
+      fail("A NULL element must not be stored as zero");
+    } catch (ConstraintViolationException expected) {
+    }
+    try {
+      PVectorDouble.INSTANCE.toObject(new Object[] { 1.0, null }, 
PDoubleArray.INSTANCE);
+      fail("A NULL element must not be stored as zero");
+    } catch (ConstraintViolationException expected) {
+    }
+  }
+}

Reply via email to