Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
cf28195
Support Oracle db VECTOR type
n0tl3ss Dec 8, 2025
70359a5
refactor Vector type
n0tl3ss Dec 10, 2025
efd704e
remove gradle properties
n0tl3ss Dec 10, 2025
cd72557
add support for multiple dialects and adds postgres vectors
n0tl3ss Dec 11, 2025
d4913f4
Merge branch '5.0.x' into support-vector-type-oracledb
n0tl3ss Dec 11, 2025
0098291
fix requires
n0tl3ss Dec 11, 2025
8196182
refactor for r2dbc
n0tl3ss Dec 12, 2025
3996937
fix tests
n0tl3ss Dec 12, 2025
683b1dd
fix tests
n0tl3ss Dec 12, 2025
f3268f0
fix impl
n0tl3ss Dec 12, 2025
6180793
fix postgres
n0tl3ss Dec 13, 2025
ab18770
fix postgres
n0tl3ss Dec 13, 2025
e59b8f1
fix test
n0tl3ss Dec 13, 2025
775a883
fix result reader
n0tl3ss Dec 13, 2025
ef095f8
remove extra annotation
n0tl3ss Dec 13, 2025
f700207
remove setup from tests
n0tl3ss Dec 13, 2025
d094e09
improve docs
n0tl3ss Dec 14, 2025
1041e83
improve docs
n0tl3ss Dec 14, 2025
f4eee69
fix tests
n0tl3ss Dec 14, 2025
2e2a76b
fix tests
n0tl3ss Dec 14, 2025
5ab85e7
cleanup
n0tl3ss Dec 15, 2025
15d3fcb
remove mapping
n0tl3ss Dec 15, 2025
c3dab64
fix bugs and duplicate code
n0tl3ss Dec 15, 2025
b68fbf1
fix test and duplication
n0tl3ss Dec 15, 2025
7dc2cb1
add more tests
n0tl3ss Dec 15, 2025
f4e92d2
fix tests
n0tl3ss Dec 15, 2025
210a290
add mysql support for vectors in jdbc
n0tl3ss Dec 17, 2025
90155bf
add improvements
n0tl3ss Dec 18, 2025
f0632e1
Merge branch '5.0.x' into support-vector-type-oracledb
n0tl3ss Dec 18, 2025
6a1dce2
use mysql connector-j instead of mysql-connector-java
n0tl3ss Dec 19, 2025
ce43e09
fix tests
n0tl3ss Dec 19, 2025
36bbe0d
Merge branch '5.0.x' into support-vector-type-oracledb
n0tl3ss Dec 19, 2025
583ce81
remove surplus
n0tl3ss Dec 19, 2025
a2d0174
fix bugs
n0tl3ss Dec 22, 2025
a9681f2
minor fixes
n0tl3ss Dec 22, 2025
7a2567f
minor refactor
n0tl3ss Dec 22, 2025
81b8f95
minor refactor
n0tl3ss Dec 22, 2025
5c752fa
minor cosmetics
n0tl3ss Dec 22, 2025
3eefef0
improve docs
n0tl3ss Dec 23, 2025
b2ed398
add kotlin and groovy examples
n0tl3ss Dec 23, 2025
43983c3
Merge branch '5.0.x' into support-vector-type-oracledb
n0tl3ss Dec 23, 2025
e3b12aa
move to Jspecific
n0tl3ss Dec 23, 2025
314fbc9
add vector indexes
n0tl3ss Dec 25, 2025
a42db6e
try to provide different image because of space on the runner
n0tl3ss Dec 26, 2025
6322b91
add more tests
n0tl3ss Dec 26, 2025
56db57e
try to remove /usr/local/lib/android on start to free more space
n0tl3ss Dec 26, 2025
acad5e0
try to remove /usr/share/dotnet on start to free more space
n0tl3ss Dec 26, 2025
0f4d038
fix OracleVectorSqlIndexDefinitionProvider
n0tl3ss Dec 26, 2025
ee54edb
fix merge
n0tl3ss Feb 24, 2026
2fba0bd
Merge branch '5.0.x' into support-vector-type-oracledb
n0tl3ss Feb 24, 2026
1fc179c
fix r2dbc
n0tl3ss Feb 24, 2026
70dada4
test build
n0tl3ss Feb 25, 2026
c2d7191
fix checkstyle
n0tl3ss Feb 25, 2026
b766655
test build
n0tl3ss Feb 25, 2026
9078515
fix test
n0tl3ss Feb 25, 2026
2bf5ccf
add suppress for S3776
n0tl3ss Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data-connection-jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ micronaut {
enabled = true
inferClasspath = false
additionalModules.add(KnownModules.JDBC_POSTGRESQL)
additionalModules.add(KnownModules.JDBC_ORACLE_XE)
clientTimeout = 300
version = libs.versions.micronaut.testresources.get()
}
Expand Down
3 changes: 2 additions & 1 deletion data-jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ dependencies {
testImplementation(mnTestResources.testcontainers.oracle.xe)

testCompileOnly mn.micronaut.inject.groovy
testImplementation mnSql.ojdbc11


testImplementation mnMultitenancy.micronaut.multitenancy

testImplementation mn.micronaut.http.netty
testRuntimeOnly mnSql.micronaut.jdbc.tomcat
testRuntimeOnly mnSql.h2
testRuntimeOnly mnSql.mariadb.java.client
testRuntimeOnly mnSql.ojdbc11
testRuntimeOnly mnSql.mysql.connector.java
testRuntimeOnly mnSql.postgresql
testRuntimeOnly mnSql.mssql.jdbc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import io.micronaut.data.model.query.builder.sql.SqlSchemaUtils;
import io.micronaut.data.model.query.builder.sql.validation.SqlTableMappingValidator;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.schema.sql.SqlTableMapping;
import io.micronaut.data.model.schema.sql.metadata.SqlColumnMetadata;
Expand Down Expand Up @@ -83,10 +85,10 @@ public class SchemaGenerator {
/**
* Constructors a schema generator for the given configurations.
*
* @param configurations The configurations
* @param schemaHandler The schema handler
* @param sqlTableMappingValidators The list of {@link SqlTableMappingValidator} instances
* @param environment The environment
* @param configurations The configurations
* @param schemaHandler The schema handler
* @param sqlTableMappingValidators The list of {@link SqlTableMappingValidator} instances
* @param environment The environment
*/
public SchemaGenerator(List<DataJdbcConfiguration> configurations,
JdbcSchemaHandler schemaHandler,
Expand Down Expand Up @@ -183,6 +185,8 @@ private static void generate(Connection connection,
PropertyPlaceholderResolver propertyPlaceholderResolver,
PersistentEntity[] entities) throws SQLException {
Dialect dialect = configuration.getDialect();
// Filter out entities that use unsupported types for the current dialect (e.g. VECTOR on non-Oracle)
entities = filterUnsupportedVectorEntities(entities, dialect);
SqlQueryBuilder builder = new SqlQueryBuilder(dialect);
if (dialect.allowBatch() && configuration.isBatchGenerate()) {
switch (configuration.getSchemaGenerate()) {
Expand Down Expand Up @@ -262,6 +266,8 @@ private static void validate(Connection connection,
PersistentEntity[] entities,
Map<Dialect, SqlTableMappingValidator> dialectSqlTableMappingValidatorMap) throws SQLException {
Dialect dialect = configuration.getDialect();
// Filter out entities that use unsupported types for the current dialect (e.g. VECTOR on non-Oracle)
entities = filterUnsupportedVectorEntities(entities, dialect);
SqlTableMappingValidator sqlTableMappingValidator = dialectSqlTableMappingValidatorMap.get(dialect);
if (sqlTableMappingValidator == null) {
throw new IllegalStateException("There is no supported SqlTableMappingValidator for dialect " + dialect);
Expand Down Expand Up @@ -387,4 +393,31 @@ private static String resolveSql(PropertyPlaceholderResolver propertyPlaceholder
}
return sql;
}

/**
* Filters out entities that contain properties mapped to DataType.VECTOR when the dialect is not ORACLE.
* This prevents schema generation errors for dialects that don't support VECTOR columns.
*/
private static PersistentEntity[] filterUnsupportedVectorEntities(PersistentEntity[] entities, Dialect dialect) {
if (dialect == Dialect.ORACLE || entities == null || entities.length == 0) {
return entities;
}
java.util.ArrayList<PersistentEntity> filtered = new java.util.ArrayList<>(entities.length);
for (PersistentEntity entity : entities) {
boolean hasVector = false;
for (PersistentProperty property : entity.getPersistentProperties()) {
if (property.getDataType() == DataType.VECTOR || property.getDataType() == DataType.VECTOR_BYTE || property.getDataType() == DataType.VECTOR_FLOAT || property.getDataType() == DataType.VECTOR_INT || property.getDataType() == DataType.VECTOR_DOUBLE) {
hasVector = true;
break;
}
}
if (!hasVector) {
filtered.add(entity);
} else if (LOG.isDebugEnabled()) {
LOG.debug("Skipping entity [{}] for dialect [{}] due to unsupported VECTOR data type", entity.getName(), dialect);
}
}
return filtered.toArray(PersistentEntity[]::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import io.micronaut.context.annotation.Prototype;
import io.micronaut.context.annotation.Requires;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.model.Vector;
import io.micronaut.data.runtime.convert.DataTypeConverter;
import oracle.jdbc.OracleType;
import oracle.sql.DATE;
import oracle.sql.TIMESTAMP;
import oracle.sql.VECTOR;

import java.sql.SQLException;
import java.sql.Timestamp;
Expand Down Expand Up @@ -87,4 +90,30 @@ DataTypeConverter<TIMESTAMP, Instant> fromOracleTimestampToInstant() {
};
}

@Prototype
DataTypeConverter<VECTOR, Vector> fromOracleVectorToVector() {
return (oracleVector, targetType, context) -> {
try {
OracleType type = oracleVector.getType();
switch (type) {
case VECTOR_FLOAT32 -> {
return Optional.of(Vector.of(oracleVector.toFloatArray()));
}
case VECTOR_FLOAT64 -> {
return Optional.of(Vector.of(oracleVector.toDoubleArray()));
}
case VECTOR_INT8 -> {
return Optional.of(Vector.of(oracleVector.toIntArray()));
}
case VECTOR_BINARY -> {
return Optional.of(Vector.of(oracleVector.toByteArray()));
}
default -> throw new DataAccessException("Cannot extract vector from: " + oracleVector);
}
} catch (SQLException e) {
throw new DataAccessException("Cannot extract vector from: " + oracleVector);
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ public <T> T getRequiredValue(ResultSet resultSet, String name, Class<T> type) t
o = resultSet.getBlob(name);
} else if (Clob.class.isAssignableFrom(type)) {
o = resultSet.getClob(name);
} else if (type == float[].class || type == double[].class || type == byte[].class || type == int[].class) {
o = resultSet.getObject(name, type);
} else {
o = resultSet.getObject(name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package io.micronaut.data.jdbc.oraclexe.vector

import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Parameter
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Query
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.jdbc.oraclexe.OracleTestPropertyProvider
import io.micronaut.data.model.Sort
import io.micronaut.data.model.Vector
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.PageableRepository
import io.micronaut.transaction.SynchronousTransactionManager
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

import javax.sql.DataSource
import java.sql.Connection
import java.sql.Statement
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit

class OracleJdbcByteVectorEntitySpec extends Specification implements OracleTestPropertyProvider {

@AutoCleanup
@Shared
ApplicationContext context = ApplicationContext.run(properties)

@Shared
VectorByteDocRepository vectorRepository = context.getBean(VectorByteDocRepository)

@Shared
DataSource dataSource = context.getBean(DataSource)

@Override
List<String> packages() {
// Ensure entity/repository in this package are scanned
return [getClass().package.name]
}

def setupSpec() {
// Create sequence and table if not exists (ignore errors if already present)
executeSilently "CREATE SEQUENCE VECTOR_DOC_SEQ"
// Oracle 23ai VECTOR: use 3 dims for tests (INT8)
executeSilently "CREATE TABLE vector_byte_doc (id NUMBER PRIMARY KEY, embedding VECTOR(3, BINARY))"
}

def cleanup() {
// Clean table between tests
executeSilently "DELETE FROM vector_byte_doc"
// no-op transaction boundary to flush
context.getBean(SynchronousTransactionManager).executeWrite { status -> null }
}

void "test save, find and update single entity (using custom queries with io.micronaut.data.model.Vector)"() {
given:
byte[] dv = [1, 2, -3] as byte[]
Vector.ByteVector v1 = Vector.of(dv)

when: "save via custom @Query using Vector parameter"
vectorRepository.saveCustom(v1)
def list = vectorRepository.findAll(Sort.of(Sort.Order.asc("id")))
def e = list.find { it.embedding.toByteArray().toList() == dv.toList() }

then: "entity persisted and read conversion to Micronaut Vector works"
e != null
e.id != null
e.embedding != null
e.embedding.type == Byte.TYPE
e.embedding.toByteArray().toList() == dv.toList()

when: "update via custom @Query to a new vector"
byte [] dv2 = [3, 0, 7] as byte[]
Vector.ByteVector v2 = Vector.of(dv2)
vectorRepository.updateCustom(e.id, v2)
def updated = vectorRepository.findById(e.id).orElse(null)

then:
updated != null
updated.embedding.type == Byte.TYPE
updated.embedding.toByteArray().toList() == dv2.toList()
}


void "test save, find and update multiple entities"() {
given:
Vector.ByteVector vA = Vector.of([1, 2, 3] as byte[])
Vector.ByteVector vB = Vector.of([4, 5, 6] as byte[])

when:
vectorRepository.saveCustom(vA)
vectorRepository.saveCustom(vB)
def rows = vectorRepository.findAll(Sort.of(Sort.Order.asc("id")))

then:
def idA = rows.find { it.embedding.toByteArray().toList() == [1, 2, 3] }?.id
def idB = rows.find { it.embedding.toByteArray().toList() == [4, 5, 6] }?.id
idA != null
idB != null
idA != idB

when: "update both"
Vector.ByteVector vA2 = Vector.of([7, 8, 9] as byte[])
Vector.ByteVector vB2 = Vector.of([0, -1, -2] as byte[])
vectorRepository.updateCustom(idA, vA2)
vectorRepository.updateCustom(idB, vB2)
def rows2 = vectorRepository.findAll(Sort.of(Sort.Order.asc("id")))

then:
def updatedA = rows2.find { it.id == idA }?.embedding?.toByteArray()?.toList()
def updatedB = rows2.find { it.id == idB }?.embedding?.toByteArray()?.toList()
updatedA == [7, 8, 9]
updatedB == [0, -1, -2]
}

void "test custom and async queries"() {
given:
Vector.ByteVector vec = Vector.of([10, 11, 12] as byte[])

when:
Future<Integer> saveFut = vectorRepository.saveAsync(vec)

then:
saveFut.get(10, TimeUnit.SECONDS) == 1

when:
def all = vectorRepository.findAll(Sort.of(Sort.Order.asc("id")))
def last = all.last()
Future<List<VectorByteDoc>> findFut = vectorRepository.findAsync(last.id)

then:
def found = findFut.get(10, TimeUnit.SECONDS)
found != null
found.size() == 1
found.get(0).embedding.toByteArray().toList() == [10, 11, 12]

when:
Vector.ByteVector vec2 = Vector.of([13, 14, 15] as byte[])
Future<Integer> updFut = vectorRepository.updateAsync(last.id, vec2)

then:
updFut.get(10, TimeUnit.SECONDS) != null
with(vectorRepository.findById(last.id)) {
it.isPresent()
it.get().embedding.toByteArray().toList() == [13, 14, 15]
}
}

private void executeSilently(String sql) {
Connection c = null
Statement st = null
try {
c = dataSource.getConnection()
st = c.createStatement()
st.execute(sql)
} catch (Throwable ignored) {
// ignore if already exists or unsupported in current XE version
} finally {
try { st?.close() } catch (ignored) {}
try { c?.close() } catch (ignored) {}
}
}
}

@MappedEntity("vector_byte_doc")
class VectorByteDoc {
@Id
@GeneratedValue(value = GeneratedValue.Type.SEQUENCE, ref = "VECTOR_DOC_SEQ")
Long id
Vector.ByteVector embedding

Long getId() { return id }
void setId(Long id) { this.id = id }

Vector.ByteVector getEmbedding() { return embedding }
void setEmbedding(Vector.ByteVector embedding) { this.embedding = embedding }
}

@JdbcRepository(dialect = Dialect.ORACLE)
interface VectorByteDocRepository extends PageableRepository<VectorByteDoc, Long> {

@Query("INSERT INTO vector_byte_doc(id, embedding) VALUES (VECTOR_DOC_SEQ.nextval, :vec)")
void saveCustom(@Parameter("vec") Vector vec)

@Query("INSERT INTO vector_byte_doc(id, embedding) VALUES (VECTOR_DOC_SEQ.nextval, :vec)")
void saveCustom(@Parameter("vec") Vector.ByteVector vec)

@Query("SELECT * FROM vector_byte_doc WHERE id = :id")
Optional<VectorByteDoc> findById(Long id)

@Query("UPDATE vector_byte_doc SET embedding = :vec WHERE id = :id")
void updateCustom(Long id, @Parameter("vec") Vector vec)

@Query("UPDATE vector_byte_doc SET embedding = :vec WHERE id = :id")
void updateCustom(Long id, @Parameter("vec") Vector.ByteVector vec)

@Query("INSERT INTO vector_byte_doc(id, embedding) VALUES (VECTOR_DOC_SEQ.nextval, :vec)")
Future<Integer> saveAsync(@Parameter("vec") Vector vec)

@Query("INSERT INTO vector_byte_doc(id, embedding) VALUES (VECTOR_DOC_SEQ.nextval, :vec)")
Future<Integer> saveAsync(@Parameter("vec") Vector.ByteVector vec)

@Query("SELECT * FROM vector_byte_doc WHERE id = :id")
Future<List<VectorByteDoc>> findAsync(Long id)

@Query("UPDATE vector_byte_doc SET embedding = :vec WHERE id = :id")
Future<Integer> updateAsync(Long id, @Parameter("vec") Vector vec)

@Query("UPDATE vector_byte_doc SET embedding = :vec WHERE id = :id")
Future<Integer> updateAsync(Long id, @Parameter("vec") Vector.ByteVector vec)

}
Loading