You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ap...@apache.org on 2026/10/08 03:21:00 UTC
(phoenix) 01/17: PHOENIX-7998 Phase 1: Vector data type and SQL grammar
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 4c65327b0868ca82af15c9a803fefbc3da30e9ee
Author: Andrew Purtell <ap...@apache.org>
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) <no...@anthropic.com>
---
phoenix-core-client/src/main/antlr3/PhoenixSQL.g | 44 +-
.../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 | 351 ++++++++++++++
.../apache/phoenix/schema/types/PVectorDouble.java | 133 ++++++
.../apache/phoenix/schema/types/PVectorFloat.java | 133 ++++++
.../java/org/apache/phoenix/util/SchemaUtil.java | 8 +-
.../org/apache/phoenix/end2end/VectorColumnIT.java | 441 +++++++++++++++++
.../phoenix/expression/CoerceExpressionTest.java | 2 +
.../phoenix/parse/VectorColumnParseTest.java | 222 +++++++++
.../apache/phoenix/schema/types/PDataTypeTest.java | 30 +-
.../phoenix/schema/types/VectorDataTypeTest.java | 529 +++++++++++++++++++++
17 files changed, 1957 insertions(+), 39 deletions(-)
diff --git a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
index 768eb60249..af0f0e5d9d 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);} )*
;
+// Column type specification with optional length and scale, supporting parameterized
+// VECTOR(<component type>, <dimension>) definitions.
+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/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..fde787f383 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();
+ }
+ // Enforce maximum supported vector dimension
+ 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()) {
+ // Format as VECTOR(<component_type>, <dimension>)
+ 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..6a64ac46ee 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();
+ // Vector maxLength represents dimension count rather than byte size.
+ 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;
+ }
+ // Derive dimension count from total byte size for vector types.
+ 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..c93d658374 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 given array type has numeric elements. */
+ 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..34c85b2bb9 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 data type corresponding to the given component type name, or null if
+ * unsupported.
+ */
+ 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..73a428f5fe
--- /dev/null
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
@@ -0,0 +1,351 @@
+/*
+ * 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> {
+
+ /** Maximum supported vector dimension for column definitions. */
+ 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 width 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);
+
+ /** Allocates a vector of this type with the given dimension. */
+ protected abstract T allocate(int dimension);
+
+ /** Sets one element of a vector of this type from a numeric value. */
+ protected abstract void set(T vector, int index, Number value);
+
+ /** Returns one element of a vector of this type, boxed. */
+ protected abstract Number get(T vector, int index);
+
+ /** Parses one element from its string representation. */
+ 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 written with the given sort order. */
+ protected abstract T unpack(byte[] buf, int offset, int length, SortOrder sortOrder);
+
+ @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;
+ }
+ return Bytes.compareTo(toBytes(lhs), rhsType.toBytes(rhs));
+ }
+
+ @Override
+ public byte[] toBytes(Object object) {
+ if (object == null) {
+ return ByteUtil.EMPTY_BYTE_ARRAY;
+ }
+ T vector = toVector(object);
+ 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;
+ }
+
+ /**
+ * Coerces an array or collection into a typed vector. Supports numeric arrays (primitive or
+ * boxed), Phoenix arrays, and JDBC arrays. Null elements are prohibited.
+ */
+ @Override
+ public Object toObject(Object object, PDataType actualType) {
+ if (object == null) {
+ return null;
+ }
+ if (getJavaClass().isInstance(object)) {
+ return 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 vector;
+ }
+ if (object instanceof String) {
+ return toObject((String) object);
+ }
+ if (object instanceof byte[]) {
+ byte[] b = (byte[]) object;
+ return toObject(b, 0, b.length);
+ }
+ return throwConstraintViolationException(actualType, this);
+ }
+
+ @Override
+ public boolean isCastableTo(PDataType targetType) {
+ return this.equals(targetType) || targetType.isArrayType()
+ || targetType.equals(PVarchar.INSTANCE) || super.isCastableTo(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 = toVector(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);
+ }
+
+ 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..b9c59ddea7
--- /dev/null
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
@@ -0,0 +1,133 @@
+/*
+ * 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 one element by index from a packed ascending-order vector buffer. */
+ public static double readElement(byte[] buf, int offset, int index) {
+ return Bytes.toDouble(buf, offset + index * Bytes.SIZEOF_DOUBLE);
+ }
+
+ /** Reads one element by index from a packed vector buffer with 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 one element by index from a packed ascending-order vector pointer. */
+ public static double readElement(ImmutableBytesWritable ptr, int index) {
+ return readElement(ptr.get(), ptr.getOffset(), index);
+ }
+
+ /** Decodes all elements from a packed ascending-order vector byte range. */
+ 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 written with 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
+ public boolean isCoercibleTo(PDataType targetType) {
+ return this.equals(targetType) || targetType.equals(PDoubleArray.INSTANCE)
+ || super.isCoercibleTo(targetType);
+ }
+}
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..619a719ae2
--- /dev/null
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
@@ -0,0 +1,133 @@
+/*
+ * 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 one element by index from a packed ascending-order vector buffer. */
+ public static float readElement(byte[] buf, int offset, int index) {
+ return Bytes.toFloat(buf, offset + index * Bytes.SIZEOF_FLOAT);
+ }
+
+ /** Reads one element by index from a packed vector buffer with 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 one element by index from a packed ascending-order vector pointer. */
+ public static float readElement(ImmutableBytesWritable ptr, int index) {
+ return readElement(ptr.get(), ptr.getOffset(), index);
+ }
+
+ /** Decodes all elements from a packed ascending-order vector byte range. */
+ 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 written with 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
+ public boolean isCoercibleTo(PDataType targetType) {
+ return this.equals(targetType) || targetType.equals(PFloatArray.INSTANCE)
+ || super.isCoercibleTo(targetType);
+ }
+}
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..1cce4a4afe 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();
+ }
+ // Vector maxLength represents dimension count rather than byte size.
+ 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..95be94b39c
--- /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());
+
+ // Verify uncached metadata resolution from system catalog
+ 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());
+ }
+
+ // Recreate table 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());
+
+ // Verify updated dimension on uncached connection
+ 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))");
+ // Integer array literal coercion
+ conn.createStatement()
+ .execute("UPSERT INTO " + tableName + " VALUES (1, ARRAY[1, 0, -2], ARRAY[3, 4, 5])");
+ // Cross-precision numeric array coercion
+ 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))");
+ // Verify null elements in literal arrays trigger 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/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..795d7b2d0b
--- /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 for parsing 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) {
+ // Expected parse error due to missing 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 {
+ // Verify non-reserved status of VECTOR identifier in table, column, family, and alias positions
+ 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());
+
+ // Column named VECTOR with 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());
+
+ // Dynamic column named VECTOR with non-vector 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..b2ab665305 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,45 +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]}",
+ + "VARCHAR ARRAY=[BINARY ARRAY, CHAR ARRAY, VARBINARY ARRAY, VARCHAR ARRAY], "
+ + "VECTOR(DOUBLE)=[DOUBLE ARRAY, VARBINARY, VARBINARY_ENCODED, VECTOR(DOUBLE)], "
+ + "VECTOR(FLOAT)=[FLOAT ARRAY, VARBINARY, VARBINARY_ENCODED, VECTOR(FLOAT)]}",
coercibleToMap.toString());
}
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..9ed8b4d29d
--- /dev/null
+++ b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
@@ -0,0 +1,529 @@
+/*
+ * 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 for 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));
+ assertTrue(type.isCoercibleTo(PFloatArray.INSTANCE));
+ assertFalse(type.isCoercibleTo(PVarchar.INSTANCE));
+ assertTrue(type.isCastableTo(PFloatArray.INSTANCE));
+ assertTrue(type.isCastableTo(PVarchar.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));
+ assertTrue(type.isCoercibleTo(PDoubleArray.INSTANCE));
+ assertFalse(type.isCoercibleTo(PVarchar.INSTANCE));
+ assertTrue(type.isCastableTo(PDoubleArray.INSTANCE));
+ assertTrue(type.isCastableTo(PVarchar.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 = PVectorFloat.INSTANCE.toBytes(original);
+ assertEquals(original.length * Bytes.SIZEOF_FLOAT, encoded.length);
+
+ 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 = PVectorDouble.INSTANCE.toBytes(original);
+ assertEquals(original.length * Bytes.SIZEOF_DOUBLE, encoded.length);
+
+ 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]; // Offset padding
+ 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);
+ }
+
+ @Test
+ public void testZeroCopyL2DistancePattern() {
+ // Direct buffer Euclidean distance calculation
+ float[] a = { 3.0f, 0.0f };
+ float[] b = { 0.0f, 4.0f };
+
+ byte[] bytesA = PVectorFloat.INSTANCE.toBytes(a);
+ byte[] bytesB = PVectorFloat.INSTANCE.toBytes(b);
+ int dim = bytesA.length / Bytes.SIZEOF_FLOAT;
+
+ double sumSq = 0.0;
+ for (int i = 0; i < dim; i++) {
+ float ai = PVectorFloat.readElement(bytesA, 0, i);
+ float bi = PVectorFloat.readElement(bytesB, 0, i);
+ double d = ai - bi;
+ sumSq += d * d;
+ }
+ assertEquals(5.0, Math.sqrt(sumSq), 1e-6);
+ }
+
+ @Test
+ public void testZeroCopyL2DistancePatternDouble() {
+ // Direct buffer Euclidean distance calculation for double vectors
+ double[] a = { 3.0, 0.0 };
+ double[] b = { 0.0, 4.0 };
+
+ byte[] bytesA = PVectorDouble.INSTANCE.toBytes(a);
+ byte[] bytesB = PVectorDouble.INSTANCE.toBytes(b);
+ int dim = bytesA.length / Bytes.SIZEOF_DOUBLE;
+
+ double sumSq = 0.0;
+ for (int i = 0; i < dim; i++) {
+ double ai = PVectorDouble.readElement(bytesA, 0, i);
+ double bi = PVectorDouble.readElement(bytesB, 0, i);
+ double d = ai - bi;
+ sumSq += d * d;
+ }
+ assertEquals(5.0, Math.sqrt(sumSq), 1e-9);
+ }
+
+ @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 array coercion 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) {
+ }
+ }
+}