Skip to content

GH-180 Introduced YdbType complement type-safe variant #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions hibernate-dialect/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
<junit5.version>5.9.3</junit5.version>
<log4j2.version>2.17.2</log4j2.version>

<ydb.sdk.version>2.3.4</ydb.sdk.version>
<ydb.jdbc.version>2.3.3</ydb.jdbc.version>
<ydb.sdk.version>2.3.13</ydb.sdk.version>
<ydb.jdbc.version>2.3.10</ydb.jdbc.version>
</properties>

<licenses>
Expand Down Expand Up @@ -146,7 +146,6 @@
<environmentVariables>
<TESTCONTAINERS_REUSE_ENABLE>true</TESTCONTAINERS_REUSE_ENABLE>
<YDB_DOCKER_FEATURE_FLAGS>enable_parameterized_decimal</YDB_DOCKER_FEATURE_FLAGS>
<YDB_DOCKER_IMAGE>cr.yandex/yc/yandex-docker-local-ydb:trunk</YDB_DOCKER_IMAGE>
</environmentVariables>
</configuration>
</plugin>
Expand Down
20 changes: 6 additions & 14 deletions spring-data-jdbc-ydb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@
<maven.compiler.source>17</maven.compiler.source>

<junit5.version>5.10.2</junit5.version>
<lombok.version>1.18.30</lombok.version>
<spring.version>3.4.0</spring.version>
<liquibase.version>4.24.0</liquibase.version>

<ydb.sdk.version>2.2.9</ydb.sdk.version>
<ydb.jdbc.version>2.2.3</ydb.jdbc.version>
<ydb.liquibase.version>0.9.7</ydb.liquibase.version>
<ydb.sdk.version>2.3.13</ydb.sdk.version>
<ydb.jdbc.version>2.3.10</ydb.jdbc.version>
<ydb.liquibase.version>1.1.1</ydb.liquibase.version>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -84,12 +83,6 @@
<artifactId>spring-data-jdbc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>tech.ydb.jdbc</groupId>
<artifactId>ydb-jdbc-driver</artifactId>
<version>${ydb.jdbc.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>tech.ydb.test</groupId>
Expand All @@ -102,9 +95,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<groupId>tech.ydb.jdbc</groupId>
<artifactId>ydb-jdbc-driver</artifactId>
<version>${ydb.jdbc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -126,7 +119,6 @@
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,75 @@
package tech.ydb.data.core.convert;

import java.sql.SQLType;
import tech.ydb.jdbc.YdbConst;
import tech.ydb.table.values.PrimitiveType;

/**
* @author Madiyar Nurgazin
*/
public record YQLType(PrimitiveType type) implements SQLType {
@Override
public String getName() {
return type.name();
}

@Override
public String getVendor() {
return "YDB";
public enum YQLType {
/** Boolean value. */
Bool(YdbConst.SQL_KIND_PRIMITIVE + 0),
/** A signed integer. Acceptable values: from -2^7 to 2^7–1. Not supported for table columns */
Int8(YdbConst.SQL_KIND_PRIMITIVE + 1),
/** An unsigned integer. Acceptable values: from 0 to 2^8–1. */
Uint8(YdbConst.SQL_KIND_PRIMITIVE + 2),
/** A signed integer. Acceptable values: from –2^15 to 2^15–1. Not supported for table columns */
Int16(YdbConst.SQL_KIND_PRIMITIVE + 3),
/** An unsigned integer. Acceptable values: from 0 to 2^16–1. Not supported for table columns */
Uint16(YdbConst.SQL_KIND_PRIMITIVE + 4),
/** A signed integer. Acceptable values: from –2^31 to 2^31–1. */
Int32(YdbConst.SQL_KIND_PRIMITIVE + 5),
/** An unsigned integer. Acceptable values: from 0 to 2^32–1. */
Uint32(YdbConst.SQL_KIND_PRIMITIVE + 6),
/** A signed integer. Acceptable values: from –2^63 to 2^63–1. */
Int64(YdbConst.SQL_KIND_PRIMITIVE + 7),
/** An unsigned integer. Acceptable values: from 0 to 2^64–1. */
Uint64(YdbConst.SQL_KIND_PRIMITIVE + 8),
/** A real number with variable precision, 4 bytes in size. Can't be used in the primary key */
Float(YdbConst.SQL_KIND_PRIMITIVE + 9),
/** A real number with variable precision, 8 bytes in size. Can't be used in the primary key */
Double(YdbConst.SQL_KIND_PRIMITIVE + 10),
/** A binary data, synonym for YDB type String */
Bytes(YdbConst.SQL_KIND_PRIMITIVE + 11),
/** Text encoded in UTF-8, synonym for YDB type Utf8 */
Text(YdbConst.SQL_KIND_PRIMITIVE + 12),
/** YSON in a textual or binary representation. Doesn't support matching, can't be used in the primary key */
Yson(YdbConst.SQL_KIND_PRIMITIVE + 13),
/** JSON represented as text. Doesn't support matching, can't be used in the primary key */
Json(YdbConst.SQL_KIND_PRIMITIVE + 14),
/** Universally unique identifier UUID. Not supported for table columns */
Uuid(YdbConst.SQL_KIND_PRIMITIVE + 15),
/** Date, precision to the day */
Date(YdbConst.SQL_KIND_PRIMITIVE + 16),
/** Date/time, precision to the second */
Datetime(YdbConst.SQL_KIND_PRIMITIVE + 17),
/** Date/time, precision to the microsecond */
Timestamp(YdbConst.SQL_KIND_PRIMITIVE + 18),
/** Time interval (signed), precision to microseconds */
Interval(YdbConst.SQL_KIND_PRIMITIVE + 19),
/** Date with time zone label, precision to the day */
TzDate(YdbConst.SQL_KIND_PRIMITIVE + 20),
/** Date/time with time zone label, precision to the second */
TzDatetime(YdbConst.SQL_KIND_PRIMITIVE + 21),
/** Date/time with time zone label, precision to the microsecond */
TzTimestamp(YdbConst.SQL_KIND_PRIMITIVE + 22),
/** JSON in an indexed binary representation. Doesn't support matching, can't be used in the primary key */
JsonDocument(YdbConst.SQL_KIND_PRIMITIVE + 23),

// DyNumber(YdbConst.SQL_KIND_PRIMITIVE + 24), -- not supported by JDBC Driver

Date32(YdbConst.SQL_KIND_PRIMITIVE + 25),

Datetime64(YdbConst.SQL_KIND_PRIMITIVE + 26),

Timestamp64(YdbConst.SQL_KIND_PRIMITIVE + 27),

Interval64(YdbConst.SQL_KIND_PRIMITIVE + 28),

Decimal(YdbConst.SQL_DEFAULT_DECIMAL); // special case

private final int sqlType;

private YQLType(int sqlType) {
this.sqlType = sqlType;
}

@Override
public Integer getVendorTypeNumber() {
return YdbConst.SQL_KIND_PRIMITIVE + type.ordinal();
public int getSqlType() {
return this.sqlType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tech.ydb.data.core.convert;

/**
* That class contain custom YDB type codes
* @see <a href="https://github.com/ydb-platform/ydb-jdbc-driver/blob/3d74021/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java#L8-L9">JDBC Driver constants</a>
* @see <a href="https://github.com/ydb-platform/ydb-jdbc-driver/blob/3d74021/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbTypes.java#L37-L66">Primitive types</a>
* @see <a href="https://github.com/ydb-platform/ydb-jdbc-driver/blob/3d74021/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbTypes.java#L138-L144">Decimal type</a>
*
* @author Aleksandr Gorshenin
*/
final class YdbConst {
public static final int SQL_KIND_PRIMITIVE = 10000;
public static final int SQL_DEFAULT_DECIMAL = ydbDecimal(22, 9);
private static final int SQL_KIND_DECIMAL = 1 << 14; // 16384

public static int ydbDecimal(int precision, int scale) {
return SQL_KIND_DECIMAL + (precision << 6) + (scale & 0x111111);
}

private YdbConst() { };
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
package tech.ydb.data.core.convert;

import java.sql.SQLType;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.data.convert.CustomConversions;
import org.springframework.data.jdbc.core.convert.JdbcTypeFactory;
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import tech.ydb.table.values.PrimitiveType;

import tech.ydb.data.core.convert.annotation.YdbType;


/**
* @author Madiyar Nurgazin
* @author Mikhail Polivakha
*/
@SuppressWarnings("removal")
public class YdbMappingJdbcConverter extends MappingJdbcConverter {
private final static Class<YdbType> ANNOTATION = YdbType.class;
private final static Class<tech.ydb.data.core.convert.YdbType> OLD_TYPE = tech.ydb.data.core.convert.YdbType.class;

private final ConcurrentMap<RelationalPersistentProperty, SQLType> typesCache = new ConcurrentHashMap<>();

public YdbMappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
CustomConversions conversions, JdbcTypeFactory typeFactory) {
super(context, relationResolver, conversions, typeFactory);
}

@Override
public SQLType getTargetSqlType(RelationalPersistentProperty property) {
return property.isAnnotationPresent(YdbType.class) ?
new YQLType(PrimitiveType.valueOf(property.getRequiredAnnotation(YdbType.class).value())) :
super.getTargetSqlType(property);
return typesCache.computeIfAbsent(property, this::resolveSqlType);
}

private SQLType resolveSqlType(RelationalPersistentProperty property) {
if (property.isAnnotationPresent(ANNOTATION)) {
tech.ydb.data.core.convert.annotation.YdbType type = property.getRequiredAnnotation(ANNOTATION);
YQLType yql = type.value();
if (yql == YQLType.Decimal) {
int precision = type.decimalPrecision();
int scale = type.decimalScale();
return new YdbSqlType(precision, scale);
}
return new YdbSqlType(yql);
}

if (property.isAnnotationPresent(OLD_TYPE)) {
String typeName = property.getRequiredAnnotation(OLD_TYPE).value();
return new YdbSqlType(YQLType.valueOf(typeName));
}

return super.getTargetSqlType(property);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package tech.ydb.data.core.convert;

import java.io.Serializable;
import java.sql.SQLType;

/**
*
* @author Aleksandr Gorshenin
*/
class YdbSqlType implements SQLType, Serializable {
private static final long serialVersionUID = -5722445668088782880L;

private final String name;
private final int vendorCode;

public YdbSqlType(YQLType type) {
this.name = type.name();
this.vendorCode = type.getSqlType();
}

public YdbSqlType(int decimalPrecision, int decimalScale) {
this.name = "Decimal(" + decimalPrecision + "," + decimalScale + ")";
this.vendorCode = YdbConst.ydbDecimal(decimalPrecision, decimalScale);
}

@Override
public String getName() {
return name;
}

@Override
public String getVendor() {
return "YDB";
}

@Override
public Integer getVendorTypeNumber() {
return vendorCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package tech.ydb.data.core.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* The annotation for qualification of the target YDB data type.
*
* @author Madiyar Nurgazin
* @author Mikhail Polivakha
* @deprecated Please, use {@link tech.ydb.data.core.convert.annotation.YdbType} instead because of type safety considerations.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Deprecated(forRemoval = true)
@Documented
public @interface YdbType {
/**
* The target YDB data type.
* @return name of YDB data type
*/
String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package tech.ydb.data.core.convert.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import tech.ydb.data.core.convert.YQLType;


/**
* The annotation for qualification of the target YDB data type.
*
* @author Mikhail Polivakha
* @author Aleksandr Gorshenin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface YdbType {
/**
* The target YDB data type.
* @return The target YDB data type.
*/
YQLType value();

/**
* Decimal precision. Applies only to {@link YQLType#Decimal }
* @return Custom decimal type precision.
*/
int decimalPrecision() default 22;

/**
* Decimal scale. Applies only to {@link YQLType#Decimal }
* @return Custom decimal type scale.
*/
int decimalScale() default 9;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.relational.core.conversion.DbActionExecutionException;

import tech.ydb.data.YdbBaseTest;
import tech.ydb.data.all_types_table.entity.AllTypesEntity;
import tech.ydb.data.all_types_table.repository.AllTypesEntityRepository;
Expand Down Expand Up @@ -41,6 +43,7 @@ public void allTypesTableCrudTest() {
);
repository.save(expected);

Assertions.assertEquals(expected.getDecimalColumn(), entity1.get().getDecimalColumn());
Assertions.assertEquals(expected, entity1.get());
Assertions.assertEquals(expected.getTextColumn(),
repository.findAllByTextColumn("Madiyar Nurgazin").get(0).getTextColumn());
Expand Down Expand Up @@ -69,10 +72,11 @@ public void allTypesTableCrudTest() {

entities = repository.findAllByDateColumnAfterNow();
Assertions.assertEquals(1, entities.size());
Assertions.assertEquals(4, entities.get(0).getId());
Assertions.assertEquals(Integer.valueOf(4), entities.get(0).getId());

entity3.setJsonColumn("Not json");
Assertions.assertThrows(DbActionExecutionException.class, () -> repository.save(entity3));
var ex = Assertions.assertThrows(DbActionExecutionException.class, () -> repository.save(entity3));
Assertions.assertTrue(ex.getMessage().startsWith("Failed to execute DbAction.UpdateRoot"));

entity3.setJsonColumn("{\"values\": [1, 2, 3]}");
AllTypesEntity updated = repository.save(entity3);
Expand Down
Loading