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