diff --git a/composites/ebean-pgvector/pom.xml b/composites/ebean-pgvector/pom.xml new file mode 100644 index 0000000000..2aece5d752 --- /dev/null +++ b/composites/ebean-pgvector/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + ebean-parent + io.ebean + 16.1.1 + ../.. + + + ebean-pgvector + ebean-pgvector composite + ebean-pgvector + + + 0.1.6 + 42.7.2 + + + + + + io.ebean + ebean-api + 16.1.1 + + + + io.ebean + ebean-core + 16.1.1 + + + + io.ebean + ebean-datasource + ${ebean-datasource.version} + + + + io.ebean + ebean-migration + ${ebean-migration.version} + + + + + io.ebean + ebean-querybean + 16.1.1 + + + + io.ebean + ebean-platform-postgres + 16.1.1 + + + + io.ebean + ebean-pgvector-types + 16.1.1 + + + + org.postgresql + postgresql + ${postgres.jdbc.version} + + + + * + * + + + + + + com.pgvector + pgvector + ${pgvector.version} + + + + diff --git a/composites/ebean-pgvector/src/main/java/io/ebean/pgvector/assembly/Assembly.java b/composites/ebean-pgvector/src/main/java/io/ebean/pgvector/assembly/Assembly.java new file mode 100644 index 0000000000..f4d7a7b0ad --- /dev/null +++ b/composites/ebean-pgvector/src/main/java/io/ebean/pgvector/assembly/Assembly.java @@ -0,0 +1,7 @@ +package io.ebean.pgvector.assembly; + +/** + * Nothing interesting here - required placeholder for javadoc. + */ +public class Assembly { +} diff --git a/composites/ebean-pgvector/src/main/java/module-info.java b/composites/ebean-pgvector/src/main/java/module-info.java new file mode 100644 index 0000000000..1937be9827 --- /dev/null +++ b/composites/ebean-pgvector/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module io.ebean.pgvector { + + requires transitive io.ebean.api; + requires transitive io.ebean.core; + requires transitive io.ebean.datasource; + requires transitive io.ebean.querybean; + requires transitive io.ebean.platform.postgres; + +} diff --git a/composites/pom.xml b/composites/pom.xml index 24ba6ce76c..69a176b86a 100644 --- a/composites/pom.xml +++ b/composites/pom.xml @@ -25,6 +25,7 @@ ebean-postgres ebean-postgis ebean-net-postgis + ebean-pgvector ebean-sqlite ebean-sqlserver diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DbPlatformTypeMapping.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DbPlatformTypeMapping.java index 3dfbdf7487..dfb5d885fb 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DbPlatformTypeMapping.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DbPlatformTypeMapping.java @@ -46,6 +46,11 @@ protected void renderLengthScale(int deployLength, int deployScale, StringBuilde private static final DbPlatformType MULTILINESTRING = new DbPlatformType("multilinestring"); private static final DbPlatformType MULTIPOLYGON = new DbPlatformType("multipolygon"); + private static final DbPlatformType VECTOR = new DbPlatformType("vector", 2000, null); + private static final DbPlatformType VECTOR_HALF = new DbPlatformType("halfvec", 4000, null); + private static final DbPlatformType VECTOR_BIT = new DbPlatformType("bit", 64000, null); + private static final DbPlatformType VECTOR_SPARSE = new DbPlatformType("sparsevec", 1000, null); + private final Map typeMap = new EnumMap<>(DbType.class); /** @@ -93,6 +98,10 @@ private void loadDefaults(boolean logicalTypes) { put(DbType.MULTIPOINT, MULTIPOINT); put(DbType.MULTILINESTRING, MULTILINESTRING); put(DbType.MULTIPOLYGON, MULTIPOLYGON); + put(DbType.VECTOR, VECTOR); + put(DbType.VECTOR_HALF, VECTOR_HALF); + put(DbType.VECTOR_BIT, VECTOR_BIT); + put(DbType.VECTOR_SPARSE, VECTOR_SPARSE); if (logicalTypes) { // keep it logical for 2 layer DDL generation diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DbType.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DbType.java index eee8aa44b3..74ef868c56 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DbType.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DbType.java @@ -51,7 +51,12 @@ public enum DbType { JSONB(ExtraDbTypes.JSONB), JSONCLOB(ExtraDbTypes.JSONClob), JSONBLOB(ExtraDbTypes.JSONBlob), - JSONVARCHAR(ExtraDbTypes.JSONVarchar); + JSONVARCHAR(ExtraDbTypes.JSONVarchar), + + VECTOR(ExtraDbTypes.VECTOR), + VECTOR_HALF(ExtraDbTypes.VECTOR_HALF), + VECTOR_BIT(ExtraDbTypes.VECTOR_BIT), + VECTOR_SPARSE(ExtraDbTypes.VECTOR_SPARSE); private final int id; diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/ExtraDbTypes.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/ExtraDbTypes.java index 91f77fdbea..447205f696 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/ExtraDbTypes.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/ExtraDbTypes.java @@ -74,4 +74,24 @@ public interface ExtraDbTypes { */ int MULTILINESTRING = 6007; + /** + * PGVector base type + */ + int VECTOR = 7000; + + /** + * PGVector half precision float type + */ + int VECTOR_HALF = 7001; + + /** + * PGVector binary type (bit) + */ + int VECTOR_BIT = 7002; + + /** + * PGVector sparse type + */ + int VECTOR_SPARSE = 7003; + } diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index 02c1caffe9..a941551273 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -256,6 +256,18 @@ 16.1.1 + + io.ebean + ebean-pgvector + 16.1.1 + + + + io.ebean + ebean-pgvector-types + 16.1.1 + + io.ebean ebean-sqlite diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java index b83fffe6df..a049adc305 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -329,6 +329,13 @@ private void bindSimpleData(DataBind b, int dataType, Object data) { geoTypeBinder.bind(b, dataType, data); break; + case DbPlatformType.VECTOR: + case DbPlatformType.VECTOR_HALF: + case DbPlatformType.VECTOR_BIT: + case DbPlatformType.VECTOR_SPARSE: + b.setObject(data); + break; + case java.sql.Types.OTHER: b.setObject(data, dataType); break; diff --git a/ebean-core/src/main/java/module-info.java b/ebean-core/src/main/java/module-info.java index 47f425cb6d..572b535e89 100644 --- a/ebean-core/src/main/java/module-info.java +++ b/ebean-core/src/main/java/module-info.java @@ -69,7 +69,7 @@ exports io.ebeaninternal.server.querydefn to io.ebean.autotune, io.ebean.querybean, io.ebean.test, io.ebean.elastic; exports io.ebeaninternal.server.rawsql to io.ebean.test; exports io.ebeaninternal.server.json to io.ebean.test, io.ebean.elastic; - exports io.ebeaninternal.server.type to io.ebean.postgis, io.ebean.test, io.ebean.postgis.types; + exports io.ebeaninternal.server.type to io.ebean.postgis, io.ebean.test, io.ebean.postgis.types, io.ebean.pgvector; exports io.ebeaninternal.server.transaction to io.ebean.test, io.ebean.elastic, io.ebean.spring.txn, io.ebean.k8scache; exports io.ebeaninternal.server.util to io.ebean.querybean; diff --git a/ebean-pgvector-types/.editorconfig b/ebean-pgvector-types/.editorconfig new file mode 100644 index 0000000000..d67326c696 --- /dev/null +++ b/ebean-pgvector-types/.editorconfig @@ -0,0 +1,17 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +spaces_around_operators = true +max_line_length = 130 + +[pom.xml] +# Because of + + 4.0.0 + + ebean-parent + io.ebean + 16.1.1 + + + ebean pgvector types + ebean-pgvector-types + + + 0.1.6 + + + + + + io.ebean + ebean-platform-postgres + 16.1.1 + + + + + io.ebean + ebean-core + 16.1.1 + provided + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + true + + + + com.pgvector + pgvector + ${pgvector.version} + + + + + org.postgresql + postgresql + 42.7.2 + provided + + + + io.ebean + ebean-test + 16.1.1 + test + + + + org.avaje.composite + logback + 1.1 + test + + + + + + + + + + io.ebean + ebean-maven-plugin + ${ebean-maven-plugin.version} + + + test + process-test-classes + + debug=0 + + + testEnhance + + + + + + + + + + diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorExtraTypeFactory.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorExtraTypeFactory.java new file mode 100644 index 0000000000..993805bbc6 --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorExtraTypeFactory.java @@ -0,0 +1,20 @@ +package io.ebean.pgvector; + +import io.ebean.DatabaseBuilder; +import io.ebean.core.type.ExtraTypeFactory; +import io.ebean.core.type.ScalarType; + +import java.util.List; + +public final class PGvectorExtraTypeFactory implements ExtraTypeFactory { + + @Override + public List> createTypes(DatabaseBuilder.Settings config, Object objectMapper) { + return List.of( + new ScalarTypePGvector(), + new ScalarTypePGhalfvec(), + new ScalarTypePGsparsevec(), + new ScalarTypePGbit() + ); + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorNewConnectionInitializer.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorNewConnectionInitializer.java new file mode 100644 index 0000000000..af454d5854 --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/PGvectorNewConnectionInitializer.java @@ -0,0 +1,21 @@ +package io.ebean.pgvector; + +import com.pgvector.PGbit; +import com.pgvector.PGvector; +import io.ebean.datasource.NewConnectionInitializer; + +import java.sql.Connection; +import java.sql.SQLException; + +public final class PGvectorNewConnectionInitializer implements NewConnectionInitializer { + + @Override + public void preInitialize(Connection connection) { + try { + PGvector.registerTypes(connection); + PGbit.registerType(connection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java new file mode 100644 index 0000000000..4bb7baa34a --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java @@ -0,0 +1,96 @@ +package io.ebean.pgvector; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.ScalarType; +import org.postgresql.util.PGobject; + +import java.io.DataInput; +import java.io.DataOutput; +import java.sql.SQLException; +import java.sql.Types; + +abstract class ScalarTypePGbase implements ScalarType { + + private final int jdbcType; + private final Class cls; + + public ScalarTypePGbase(int jdbcType, Class cls) { + this.jdbcType = jdbcType; + this.cls = cls; + } + + @Override + public T read(DataReader reader) throws SQLException { + var obj=reader.getObject(); + if(obj==null) return null; + return cls.cast(obj); + } + + @Override + public void bind(DataBinder binder, T value) throws SQLException { + if(value==null) { + binder.setNull(Types.NULL); + } else { + binder.setObject(value); + } + } + + @Override + public boolean jdbcNative() { + return true; + } + + @Override + public int jdbcType() { + return jdbcType; + } + + @Override + public Class type() { + return cls; + } + + @Override + public T readData(DataInput dataInput) { + return null; + } + + @Override + public void writeData(DataOutput dataOutput, T v) { + + } + + @Override + public Object toJdbcType(Object value) { + return null; + } + + @Override + public T toBeanType(Object value) { + return null; + } + + @Override + public String formatValue(T value) { + return value.toString(); + } + + @Override + public DocPropertyType docType() { + return null; + } + + @Override + public T jsonRead(JsonParser parser) { + return null; + } + + @Override + public void jsonWrite(JsonGenerator writer, T value) { + + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbit.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbit.java new file mode 100644 index 0000000000..1a0593fd64 --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbit.java @@ -0,0 +1,22 @@ +package io.ebean.pgvector; + +import com.pgvector.PGbit; +import io.ebean.config.dbplatform.ExtraDbTypes; + +import java.sql.SQLException; + +public final class ScalarTypePGbit extends ScalarTypePGbase { + + public ScalarTypePGbit() { + super(ExtraDbTypes.VECTOR_BIT, PGbit.class); + } + + @Override + public PGbit parse(String value) { + try { + return new PGbit(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGhalfvec.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGhalfvec.java new file mode 100644 index 0000000000..65f8a05a6d --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGhalfvec.java @@ -0,0 +1,22 @@ +package io.ebean.pgvector; + +import com.pgvector.PGhalfvec; +import io.ebean.config.dbplatform.ExtraDbTypes; + +import java.sql.SQLException; + +public final class ScalarTypePGhalfvec extends ScalarTypePGbase { + + public ScalarTypePGhalfvec() { + super(ExtraDbTypes.VECTOR_HALF, PGhalfvec.class); + } + + @Override + public PGhalfvec parse(String value) { + try { + return new PGhalfvec(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGsparsevec.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGsparsevec.java new file mode 100644 index 0000000000..9749b7953e --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGsparsevec.java @@ -0,0 +1,22 @@ +package io.ebean.pgvector; + +import com.pgvector.PGsparsevec; +import io.ebean.config.dbplatform.ExtraDbTypes; + +import java.sql.SQLException; + +public final class ScalarTypePGsparsevec extends ScalarTypePGbase { + + public ScalarTypePGsparsevec() { + super(ExtraDbTypes.VECTOR_SPARSE, PGsparsevec.class); + } + + @Override + public PGsparsevec parse(String value) { + try { + return new PGsparsevec(value); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGvector.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGvector.java new file mode 100644 index 0000000000..c5ca67eca9 --- /dev/null +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGvector.java @@ -0,0 +1,20 @@ +package io.ebean.pgvector; + +import com.pgvector.PGvector; +import io.ebean.config.dbplatform.ExtraDbTypes; + +public final class ScalarTypePGvector extends ScalarTypePGbase { + + public ScalarTypePGvector() { + super(ExtraDbTypes.VECTOR, PGvector.class); + } + + @Override + public PGvector parse(String value) { + try { + return new PGvector(value); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.core.type.ExtraTypeFactory b/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.core.type.ExtraTypeFactory new file mode 100644 index 0000000000..09973ed327 --- /dev/null +++ b/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.core.type.ExtraTypeFactory @@ -0,0 +1 @@ +io.ebean.pgvector.PGvectorExtraTypeFactory diff --git a/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.datasource.NewConnectionInitializer b/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.datasource.NewConnectionInitializer new file mode 100644 index 0000000000..11a2134f75 --- /dev/null +++ b/ebean-pgvector-types/src/main/resources/META-INF/services/io.ebean.datasource.NewConnectionInitializer @@ -0,0 +1 @@ +io.ebean.pgvector.PGvectorNewConnectionInitializer diff --git a/ebean-pgvector-types/src/test/java/org/example/domain/BaseEntity.java b/ebean-pgvector-types/src/test/java/org/example/domain/BaseEntity.java new file mode 100644 index 0000000000..6dc59d1e20 --- /dev/null +++ b/ebean-pgvector-types/src/test/java/org/example/domain/BaseEntity.java @@ -0,0 +1,20 @@ +package org.example.domain; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +abstract class BaseEntity { + + @Id + Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + +} diff --git a/ebean-pgvector-types/src/test/java/org/example/domain/CachedBean.java b/ebean-pgvector-types/src/test/java/org/example/domain/CachedBean.java new file mode 100644 index 0000000000..1a227eeb09 --- /dev/null +++ b/ebean-pgvector-types/src/test/java/org/example/domain/CachedBean.java @@ -0,0 +1,69 @@ +package org.example.domain; + +import com.pgvector.PGbit; +import com.pgvector.PGhalfvec; +import com.pgvector.PGsparsevec; +import com.pgvector.PGvector; +import io.ebean.annotation.Cache; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name="mybean_cached") +@Cache +public class CachedBean extends BaseEntity { + String name; + + @Column(length = 800) + PGvector vector; + + @Column(length = 200) + PGsparsevec sparsevec; + + @Column(length = 200) + PGbit bit; + + @Column(length = 200) + PGhalfvec halfvec; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PGvector getVector() { + return vector; + } + + public void setVector(PGvector vector) { + this.vector = vector; + } + + public PGsparsevec getSparsevec() { + return sparsevec; + } + + public void setSparsevec(PGsparsevec sparsevec) { + this.sparsevec = sparsevec; + } + + public PGbit getBit() { + return bit; + } + + public void setBit(PGbit bit) { + this.bit = bit; + } + + public PGhalfvec getHalfvec() { + return halfvec; + } + + public void setHalfvec(PGhalfvec halfvec) { + this.halfvec = halfvec; + } +} diff --git a/ebean-pgvector-types/src/test/java/org/example/domain/MyBean.java b/ebean-pgvector-types/src/test/java/org/example/domain/MyBean.java new file mode 100644 index 0000000000..3052b0750c --- /dev/null +++ b/ebean-pgvector-types/src/test/java/org/example/domain/MyBean.java @@ -0,0 +1,69 @@ +package org.example.domain; + +import com.pgvector.PGbit; +import com.pgvector.PGhalfvec; +import com.pgvector.PGsparsevec; +import com.pgvector.PGvector; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name="mybean") +public class MyBean extends BaseEntity { + + String name; + + @Column(length = 200) + PGvector vector; + + @Column(length = 350) + PGsparsevec sparse; + + @Column(length = 1200) + PGbit bit; + + @Column(length = 420) + PGhalfvec halfvec; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PGvector getVector() { + return vector; + } + + public void setVector(PGvector vector) { + this.vector = vector; + } + + public PGsparsevec getSparse() { + return sparse; + } + + public void setSparse(PGsparsevec sparse) { + this.sparse = sparse; + } + + public PGbit getBit() { + return bit; + } + + public void setBit(PGbit bit) { + this.bit = bit; + } + + public PGhalfvec getHalfvec() { + return halfvec; + } + + public void setHalfvec(PGhalfvec halfvec) { + this.halfvec = halfvec; + } +} diff --git a/ebean-pgvector-types/src/test/java/org/example/domain/TestCachedBean.java b/ebean-pgvector-types/src/test/java/org/example/domain/TestCachedBean.java new file mode 100644 index 0000000000..994ee98688 --- /dev/null +++ b/ebean-pgvector-types/src/test/java/org/example/domain/TestCachedBean.java @@ -0,0 +1,35 @@ +package org.example.domain; + +import com.pgvector.PGbit; +import com.pgvector.PGvector; +import io.ebean.DB; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class TestCachedBean { + + @Test + void testCache() { + var v = new PGvector(TestInsertQuery.randomVector(800)); + var b = new PGbit((TestInsertQuery.randomBitArray(200))); + CachedBean cb = new CachedBean(); + cb.setName("test"); + cb.setVector(v); + cb.setBit(b); + DB.insert(cb); + + CachedBean r1 = DB.find(CachedBean.class, cb.getId()); + assertNotNull(r1); + assertEquals(v, r1.getVector()); + assertEquals(b, r1.getBit()); + + CachedBean r2 = DB.find(CachedBean.class, cb.getId()); + assertNotNull(r2); + assertEquals(v, r2.getVector()); + assertEquals(b, r2.getBit()); + + DB.delete(r2); + } +} diff --git a/ebean-pgvector-types/src/test/java/org/example/domain/TestInsertQuery.java b/ebean-pgvector-types/src/test/java/org/example/domain/TestInsertQuery.java new file mode 100644 index 0000000000..9530440b08 --- /dev/null +++ b/ebean-pgvector-types/src/test/java/org/example/domain/TestInsertQuery.java @@ -0,0 +1,136 @@ +package org.example.domain; + +import com.pgvector.PGbit; +import com.pgvector.PGhalfvec; +import com.pgvector.PGsparsevec; +import com.pgvector.PGvector; +import io.ebean.DB; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +class TestInsertQuery { + + static float[] randomVector(int dim) { + Random rnd = new Random(); + float[] vector = new float[dim]; + for (int i = 0; i < dim; i++) { + vector[i] = rnd.nextFloat(); + } + return vector; + } + + static boolean compareHalfVectors(PGhalfvec v1, PGhalfvec v2) { + if (v1 == null || v2 == null) return v1 == v2; + float[] a1 = v1.toArray(); + float[] a2 = v2.toArray(); + if (a1.length != a2.length) return false; + for (int i = 0; i < a1.length; i++) { + if (Math.abs(a1[i] - a2[i]) > 0.001) return false; + } + return true; + } + + static float[] randomSparseVector(int dim, int nonZeroCount) { + Random rnd = new Random(); + float[] vector = new float[dim]; + for (int i = 0; i < nonZeroCount; i++) { + int index; + do { + index = rnd.nextInt(dim); + } while (vector[index] != 0); + vector[index] = rnd.nextFloat(); + } + return vector; + } + + static boolean[] randomBitArray(int length) { + Random rnd = new Random(); + boolean[] bits = new boolean[length]; + for (int i = 0; i < length; i++) { + bits[i] = rnd.nextBoolean(); + } + return bits; + } + + @Test + public void insert() { + List list = DB.find(MyBean.class).findList(); + for (MyBean MyBean : list) { + System.out.println(MyBean.getVector()); + } + + var v1 = new PGvector(randomVector(200)); + + MyBean myBean = new MyBean(); + myBean.setName("test"); + myBean.setVector(v1); + DB.save(myBean); + + var dbBean = DB.find(MyBean.class, myBean.getId()); + assertNotNull(dbBean); + assertEquals(myBean.getVector().toString(), dbBean.getVector().toString()); + } + + @Test + void differentTypes() { + var rv1 = new PGvector(randomVector(200)); + var rv2 = new PGvector(randomVector(200)); + var rh1 = new PGhalfvec(randomVector(420)); + var rh2 = new PGhalfvec(randomVector(420)); + var rb1 = new PGbit(randomBitArray(1200)); + var rb2 = new PGbit(randomBitArray(1200)); + var rs1 = new PGsparsevec(randomSparseVector(350, 2)); + var rs2 = new PGsparsevec(randomSparseVector(350, 2)); + + MyBean b1 = new MyBean(); + b1.setName("testTypes"); + b1.setVector(rv1); + b1.setHalfvec(rh1); + b1.setBit(rb1); + b1.setSparse(rs1); + DB.save(b1); + + MyBean b2 = new MyBean(); + b2.setName("testTypes2"); + b2.setVector(rv2); + b2.setHalfvec(rh2); + b2.setBit(rb2); + b2.setSparse(rs2); + DB.save(b2); + + var f1 = DB.find(MyBean.class).where().eq("vector", rv1).findOne(); + assertNotNull(f1); + assertEquals(b1.getId(), f1.getId()); + assertEquals(b1.getVector(), f1.getVector()); + assertTrue(compareHalfVectors(b1.getHalfvec(), f1.getHalfvec())); + assertEquals(b1.getBit(), f1.getBit()); + assertEquals(b1.getSparse(), f1.getSparse()); + + var f2 = DB.find(MyBean.class).where().eq("sparse", rs2).findOne(); + assertNotNull(f2); + assertEquals(b2.getId(), f2.getId()); + assertEquals(b2.getVector(), f2.getVector()); + assertTrue(compareHalfVectors(b2.getHalfvec(), f2.getHalfvec())); + assertEquals(b2.getBit(), f2.getBit()); + assertEquals(b2.getSparse(), f2.getSparse()); + + var f3 = DB.find(MyBean.class).where().eq("bit", rb1).findOne(); + assertNotNull(f3); + assertEquals(b1.getId(), f3.getId()); + assertEquals(b1.getVector(), f3.getVector()); + assertTrue(compareHalfVectors(b1.getHalfvec(), f3.getHalfvec())); + assertEquals(b1.getBit(), f3.getBit()); + assertEquals(b1.getSparse(), f3.getSparse()); + + DB.delete(f1); + + assertEquals(1, DB.find(MyBean.class).where().eq("halfvec", rh2).delete()); + assertNull(DB.find(MyBean.class).where().eq("sparse", rs2).findOne()); + assertNull(DB.find(MyBean.class).where().eq("bit", rb1).findOne()); + + } +} diff --git a/ebean-pgvector-types/src/test/resources/application-test.yml b/ebean-pgvector-types/src/test/resources/application-test.yml new file mode 100644 index 0000000000..cb42472f7d --- /dev/null +++ b/ebean-pgvector-types/src/test/resources/application-test.yml @@ -0,0 +1,14 @@ +ebean: + dbSchema: mypgvectorapp + + test: + # useDocker: false + # shutdown: stop # stop | remove + platform: pgvector + ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate + dbName: mypgvectorapp + + pgvector: + containerName: ebeanbuild_pgvector + port: 8432 + image: pgvector/pgvector:pg18 diff --git a/ebean-pgvector-types/src/test/resources/logback-test.xml b/ebean-pgvector-types/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..11ba301e03 --- /dev/null +++ b/ebean-pgvector-types/src/test/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + diff --git a/ebean-postgis-types/src/test/java/org/example/domain/TestInsertQuery.java b/ebean-postgis-types/src/test/java/org/example/domain/TestInsertQuery.java index c12d1765eb..6d35b4cecb 100644 --- a/ebean-postgis-types/src/test/java/org/example/domain/TestInsertQuery.java +++ b/ebean-postgis-types/src/test/java/org/example/domain/TestInsertQuery.java @@ -12,14 +12,13 @@ import java.sql.SQLException; import java.util.List; -public class TestInsertQuery { +class TestInsertQuery { /** * Not automated this test yet. */ @Test - public void insert() throws SQLException { - + void insert() throws SQLException { List list = DB.find(MyBean.class).findList(); for (MyBean MyBean : list) { diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/PGvectorSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/PGvectorSetup.java new file mode 100644 index 0000000000..29f4a755fc --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/PGvectorSetup.java @@ -0,0 +1,49 @@ +package io.ebean.test.config.platform; + +import java.util.Properties; + +final class PGvectorSetup implements PlatformSetup { + + @Override + public Properties setup(Config config) { + int defaultPort = config.isUseDocker() ? 8432 : 5432; + config.setDockerPlatform("pgvector"); + config.ddlMode("dropCreate"); + config.setDefaultPort(defaultPort); + config.setUsernameDefault(); + config.setPasswordDefault(); + config.setUrl("jdbc:postgresql://${host}:${port}/${databaseName}"); + String schema = config.getSchema(); + if (schema != null && !schema.equals(config.getUsername())) { + config.urlAppend("?currentSchema=" + schema); + } + config.setDriver("org.postgresql.Driver"); + config.datasourceDefaults(); + return dockerProperties(config); + } + + private Properties dockerProperties(Config config) { + if (!config.isUseDocker()) { + return new Properties(); + } + config.setExtensions("vector"); + config.setDockerContainerName("ut_pgvector"); + config.setDockerVersion("pg18"); + return config.getDockerProperties(); + } + + @Override + public void setupExtraDbDataSource(Config config) { + int defaultPort = config.isUseDocker() ? 8432 : 5432; + config.setDefaultPort(defaultPort); + config.setExtraUsernameDefault(); + config.setExtraDbPasswordDefault(); + config.setExtraUrl("jdbc:postgresql://${host}:${port}/${databaseName}"); + config.extraDatasourceDefaults(); + } + + @Override + public boolean isLocal() { + return false; + } +} diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java b/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java index 8ef841025b..0f471f81a4 100644 --- a/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java @@ -26,6 +26,7 @@ public class PlatformAutoConfig { KNOWN_PLATFORMS.put("sqlite", new SqliteSetup()); KNOWN_PLATFORMS.put("postgres", new PostgresSetup()); KNOWN_PLATFORMS.put("postgis", new PostgisSetup()); + KNOWN_PLATFORMS.put("pgvector", new PGvectorSetup()); KNOWN_PLATFORMS.put("nuodb", new NuoDBSetup()); KNOWN_PLATFORMS.put("mysql", new MySqlSetup()); KNOWN_PLATFORMS.put("mariadb", new MariaDBSetup()); diff --git a/platforms/postgres/src/main/java/io/ebean/platform/postgres/PostgresPlatform.java b/platforms/postgres/src/main/java/io/ebean/platform/postgres/PostgresPlatform.java index 463ddd9547..fec2401d8f 100644 --- a/platforms/postgres/src/main/java/io/ebean/platform/postgres/PostgresPlatform.java +++ b/platforms/postgres/src/main/java/io/ebean/platform/postgres/PostgresPlatform.java @@ -79,6 +79,11 @@ public PostgresPlatform() { dbTypeMap.put(DbType.CLOB, dbTypeText); dbTypeMap.put(DbType.LONGVARBINARY, dbBytea); dbTypeMap.put(DbType.LONGVARCHAR, dbTypeText); + + dbTypeMap.put(DbType.VECTOR, new DbPlatformType("vector", 512, 2000, null)); + dbTypeMap.put(DbType.VECTOR_HALF, new DbPlatformType("halfvec", 512, 4000, null)); + dbTypeMap.put(DbType.VECTOR_BIT, new DbPlatformType("bit", 512, 64000, null)); + dbTypeMap.put(DbType.VECTOR_SPARSE, new DbPlatformType("sparsevec", 512, 64000, null)); } @Override diff --git a/pom.xml b/pom.xml index 8e1b8b8f8e..cacdc504fa 100644 --- a/pom.xml +++ b/pom.xml @@ -48,8 +48,8 @@ 2.3 1.2 14.3.0 - 7.15 - 10.1 + 7.17 + 10.2 16.1.1 16.1.1 false @@ -91,6 +91,7 @@ ebean-querybean ebean-postgis-types ebean-net-postgis-types + ebean-pgvector-types ebean-redis platforms composites