diff --git a/jimmer-dialect/README.md b/jimmer-dialect/README.md new file mode 100644 index 00000000..a8bc89c0 --- /dev/null +++ b/jimmer-dialect/README.md @@ -0,0 +1,39 @@ +# YDB Dialect for Jimmer + +## Overview + +This project contains a custom Jimmer dialect +for a simple integration between Jimmer ORM and Yandex Database (YDB). +For more thorough integration it is recommended to use the YqlClientBuilder class +for a custom JSqlClient. + + +### Features + +- Custom type mappings to utilize YDB's data types. +- Support for YDB-specific features and functions. +- Transaction modes and isolation levels. +- Retries with exponential backoff +- YDB keyset pagination. + +### Missing Features + +- No support for YQL data type "Datetime64": the current mapping for the Java class LocalDateTime is incorrect + +## Getting Started + +### Requirements + +To use this Hibernate YDB Dialect, you'll need: + +- Java 17 or above. +- Jimmer version 0.9.117 +- [YDB JDBC Driver](https://github.com/ydb-platform/ydb-jdbc-driver) +- Access to a YDB Database instance + +## Usage + +```java +DataSource dataSource = new DriverManagerDataSource("jdbc:ydb:grpc://localhost:2136/local"); +YqlClient yqlClient = getYqlClient(dataSource); +``` \ No newline at end of file diff --git a/jimmer-dialect/pom.xml b/jimmer-dialect/pom.xml new file mode 100644 index 00000000..854edc37 --- /dev/null +++ b/jimmer-dialect/pom.xml @@ -0,0 +1,111 @@ + + 4.0.0 + + jimmer.ydb + jimmer-ydb-dialect + 1.0-SNAPSHOT + YDB dialect for Jimmer ORM + + + 0.9.117 + 7.0.2 + 1.37 + 17 + + + + + + org.babyfish.jimmer + jimmer-sql + ${jimmer.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.21.0 + test + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + + tech.ydb.jdbc + ydb-jdbc-driver + 2.3.20 + test + + + + org.springframework + spring-jdbc + ${spring.version} + test + + + + org.slf4j + slf4j-api + 2.0.17 + test + + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.25.3 + test + + + + tech.ydb.test + ydb-junit5-support + 2.3.29 + test + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + + + org.babyfish.jimmer + jimmer-apt + ${jimmer.version} + + + + -parameters + + + + + + diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/UuidTransactionCacheOperator.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/UuidTransactionCacheOperator.java new file mode 100644 index 00000000..006c9c11 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/UuidTransactionCacheOperator.java @@ -0,0 +1,131 @@ +package ydb.jimmer.dialect; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.babyfish.jimmer.meta.ImmutableProp; +import org.babyfish.jimmer.meta.ImmutableType; +import org.babyfish.jimmer.sql.cache.TransactionCacheOperator; +import org.babyfish.jimmer.sql.cache.UsedCache; +import org.babyfish.jimmer.sql.exception.ExecutionException; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +/** + * Uses UUID for the ids in the {@link #TABLE_NAME} SQL table. + */ +public class UuidTransactionCacheOperator extends TransactionCacheOperator { + public static final String TABLE_NAME = "JIMMER_TRANS_CACHE_OPERATOR"; + + private static final String ID = "ID"; + + private static final String IMMUTABLE_TYPE = "IMMUTABLE_TYPE"; + + private static final String IMMUTABLE_PROP = "IMMUTABLE_PROP"; + + private static final String CACHE_KEY = "CACHE_KEY"; + + private static final String REASON = "REASON"; + + private static final String INSERT_WITH_UUID = + "insert into " + + TABLE_NAME + "(" + + ID + + ", " + + IMMUTABLE_TYPE + + ", " + + IMMUTABLE_PROP + + ", " + + CACHE_KEY + + ", " + + REASON + + ") values(?, ?, ?, ?, ?)"; + + private final ObjectMapper mapper; + + private final int batchSize; + + public UuidTransactionCacheOperator() { + this(null, 32); + } + + public UuidTransactionCacheOperator(int batchSize) { + this(null, batchSize); + } + + public UuidTransactionCacheOperator(ObjectMapper mapper) { + this(mapper, 32); + } + + public UuidTransactionCacheOperator(ObjectMapper mapper, int batchSize) { + super(mapper, batchSize); + + if (batchSize < 1) { + throw new IllegalArgumentException("`batchSize` cannot be less than 1"); + } + this.mapper = mapper != null ? + mapper : + new ObjectMapper().findAndRegisterModules(); + this.batchSize = batchSize; + } + + @Override + public void delete(UsedCache cache, Object key, Object reason) { + if (reason != null && !(reason instanceof String)) { + throw new IllegalArgumentException( + "The cache deletion reason can only be null or string when trigger type is `TRANSACTION_ONLY`" + ); + } + save(cache.type(), cache.prop(), Collections.singleton(key), (String) reason); + } + + @Override + public void deleteAll(UsedCache cache, Collection keys, Object reason) { + if (keys.isEmpty()) { + return; + } + if (reason != null && !(reason instanceof String)) { + throw new IllegalArgumentException( + "The cache deletion reason can only be null or string when trigger type is `TRANSACTION_ONLY`" + ); + } + save(cache.type(), cache.prop(), keys, (String) reason); + } + + private void save( + ImmutableType type, + ImmutableProp prop, + Collection keys, + String reason + ) { + sqlClient().getConnectionManager().execute(con -> { + try { + try (PreparedStatement stmt = con.prepareStatement(INSERT_WITH_UUID)) { + int count = 0; + for (Object key : keys) { + stmt.setString(1, UUID.randomUUID().toString()); + stmt.setString(2, type != null ? type.toString() : null); + stmt.setString(3, prop != null ? prop.toString() : null); + stmt.setString(4, mapper.writeValueAsString(key)); + stmt.setString(5, reason); + stmt.addBatch(); + + if (++count % batchSize == 0) { + stmt.executeBatch(); + } + } + + if (count % batchSize != 0) { + stmt.executeBatch(); + } + } + } catch (SQLException | JsonProcessingException ex) { + throw new ExecutionException("Failed to save delayed cache deletion", ex); + } + return null; + }); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbDialect.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbDialect.java new file mode 100644 index 00000000..f7a91fa7 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbDialect.java @@ -0,0 +1,222 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.ast.impl.Ast; +import org.babyfish.jimmer.sql.ast.impl.Literals; +import org.babyfish.jimmer.sql.ast.impl.query.ForUpdate; +import org.babyfish.jimmer.sql.ast.impl.render.AbstractSqlBuilder; +import org.babyfish.jimmer.sql.dialect.DefaultDialect; +import org.jetbrains.annotations.Nullable; + +import java.sql.Types; + +import static ydb.jimmer.dialect.constant.YdbClassMapping.classToJdbcType; +import static ydb.jimmer.dialect.constant.YdbClassMapping.classToYdbType; + +public class YdbDialect extends DefaultDialect { + @Override + public String sqlType(Class elementType) { + return classToYdbType.get(elementType); + } + + @Override + public int resolveJdbcType(Class sqlType) { + Integer jdbcType = classToJdbcType.get(sqlType); + if (jdbcType == null) { + return Types.OTHER; + } + return jdbcType; + } + + @Override + public boolean isDeleteAliasSupported() { + return false; + } + + @Override + public boolean isUpdateAliasSupported() { + return false; + } + + @Override + public boolean isTableOfSubQueryMutable() { + return false; + } + + @Override + public boolean isForeignKeySupported() { + return false; + } + + @Override + public boolean isInsertedIdReturningRequired() { + return true; + } + + @Override + public boolean isExplicitBatchRequired() { + return true; + } + + @Override + public boolean isBatchDumb() { + return true; + } + + @Override + public boolean isUpsertSupported() { + return true; + } + + @Override + public boolean isNoIdUpsertSupported() { + return false; + } + + @Override + public boolean isUpsertWithOptimisticLockSupported() { + return true; + } + + @Override + public boolean isUpsertWithNullableKeySupported() { + return true; + } + + @Override + public void update(UpdateContext ctx) { + ctx.sql("UPDATE ") + .appendTableName() + .enter(AbstractSqlBuilder.ScopeType.SET) + .appendAssignments() + .leave() + .enter(AbstractSqlBuilder.ScopeType.WHERE) + .appendPredicates() + .leave() + .sql(" RETURNING ") + .appendId(); + } + + public void batchUpdate(UpdateContext ctx) { + update(ctx.sql("BATCH ")); + } + + @Override + public void upsert(UpsertContext ctx) { + if (ctx.isUpdateIgnored() || !ctx.hasUpdatedColumns()) { + ctx.sql("INSERT INTO ") + .appendTableName() + .enter(AbstractSqlBuilder.ScopeType.MULTIPLE_LINE_TUPLE) + .appendInsertedColumns("") + .leave() + .sql(" VALUES") + .enter(AbstractSqlBuilder.ScopeType.MULTIPLE_LINE_TUPLE) + .appendInsertingValues() + .leave(); + } else { + ctx.sql("UPSERT INTO ") + .appendTableName() + .enter(AbstractSqlBuilder.ScopeType.MULTIPLE_LINE_TUPLE) + .appendInsertedColumns("") + .leave() + .sql(" VALUES") + .enter(AbstractSqlBuilder.ScopeType.MULTIPLE_LINE_TUPLE) + .appendInsertingValues() + .leave(); + } + } + + public void bulkUpsert(UpsertContext ctx) { + upsert(ctx.sql("BULK ")); + } + + @Override + public String transCacheOperatorTableDDL() { + return """ + CREATE TABLE JIMMER_TRANS_CACHE_OPERATOR( + \tID Uuid NOT NULL, + \tIMMUTABLE_TYPE String, + \tIMMUTABLE_PROP String, + \tCACHE_KEY String NOT NULL, + \tREASON String, + \tPRIMARY KEY(ID) + )"""; + } + + @Override + public void renderLPad( + AbstractSqlBuilder builder, + int currentPrecedence, + Ast expression, + Ast length, + Ast padString + ) { + throw new UnsupportedOperationException( + "The current dialect \"" + getClass().getName() + "\" does not support LPad." + ); + } + + @Override + public void renderRPad( + AbstractSqlBuilder builder, + int currentPrecedence, + Ast expression, + Ast length, + Ast padString + ) { + throw new UnsupportedOperationException( + "The current dialect \"" + getClass().getName() + "\" does not support RPad." + ); + } + + @Override + public void renderPosition( + AbstractSqlBuilder builder, + int currentPrecedence, + Ast subStrAst, + Ast expressionAst, + @Nullable Ast startAst + ) { + builder.sql("FIND(") + .ast(expressionAst, currentPrecedence) + .sql(", ") + .ast(subStrAst, currentPrecedence); + if (startAst != null) { + builder.sql(", ") + .ast(startAst, currentPrecedence); + } + builder.sql(")"); + } + + @Override + public void renderLeft( + AbstractSqlBuilder builder, + int currentPrecedence, + Ast expressionAst, + Ast lengthAst + ) { + renderSubString(builder, currentPrecedence, expressionAst, (Ast) Literals.number(0), lengthAst); + } + + @Override + public void renderRight( + AbstractSqlBuilder builder, + int currentPrecedence, + Ast expressionAst, + Ast lengthAst + ) { + builder.sql("substring(") + .ast(expressionAst, currentPrecedence) + .sql(", (LENGTH(") + .ast(expressionAst, currentPrecedence) + .sql(") - ") + .ast(lengthAst, currentPrecedence) + .sql("))"); + } + + @Override + public void renderForUpdate(AbstractSqlBuilder builder, ForUpdate forUpdate) { + throw new UnsupportedOperationException( + "The current dialect \"" + getClass().getName() + "\" does not support 'for update' hint." + ); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbExecutor.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbExecutor.java new file mode 100644 index 00000000..2d6e37c9 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbExecutor.java @@ -0,0 +1,78 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.runtime.AbstractExecutorProxy; +import org.babyfish.jimmer.sql.runtime.ExecutionPurpose; +import org.babyfish.jimmer.sql.runtime.Executor; +import org.babyfish.jimmer.sql.runtime.SqlFunction; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Proxy; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +/** + * Takes the ResultSet returned by the "UPDATE" operator with "RETURNING" + * and returns the row count of the affected rows. + * By default, Jimmer does not support ResultSet for "UPDATE". + */ +public class YdbExecutor extends AbstractExecutorProxy { + public YdbExecutor(Executor raw) { + super(raw); + } + + @Override + public R execute(@NotNull Args args) { + if (args.purpose != ExecutionPurpose.MUTATE) { + return raw.execute(args); + } + + SqlFunction originalBlock = args.block; + Args newArgs = new Args<>( + args.sqlClient, + args.con, + args.sql, + args.variables, + args.variablePositions, + args.purpose, + args.getExceptionTranslator(), + args.statementFactory, + (stmt, a) -> originalBlock.apply(wrapStatement(stmt), a) + ); + return raw.execute(newArgs); + } + + private PreparedStatement wrapStatement(PreparedStatement stmt) { + return (PreparedStatement) Proxy.newProxyInstance( + stmt.getClass().getClassLoader(), + new Class[]{PreparedStatement.class}, + (proxy, method, methodArgs) -> { + if ("executeUpdate".equals(method.getName()) && (methodArgs == null || methodArgs.length == 0)) { + boolean hasResultSet = stmt.execute(); + if (hasResultSet) { + try (ResultSet rs = stmt.getResultSet()) { + int count = 0; + while (rs.next()) { + count++; + } + return count; + } + } + + return Math.max(stmt.getUpdateCount(), 0); + } + + return method.invoke(stmt, methodArgs); + } + ); + } + + @Override + protected AbstractExecutorProxy recreate(Executor raw) { + return new YdbExecutor(raw); + } + + @Override + protected Batch createBatch(BatchContext raw) { + return new AbstractExecutorProxy.Batch(raw) {}; + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbKeysetPaginator.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbKeysetPaginator.java new file mode 100644 index 00000000..c44f0c1f --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YdbKeysetPaginator.java @@ -0,0 +1,120 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.ast.Expression; +import org.babyfish.jimmer.sql.ast.NativeBuilder; +import org.babyfish.jimmer.sql.ast.Predicate; +import org.babyfish.jimmer.sql.ast.query.ConfigurableRootQuery; +import org.babyfish.jimmer.sql.ast.query.MutableRootQuery; +import org.babyfish.jimmer.sql.ast.table.spi.TableProxy; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class YdbKeysetPaginator { + private final JSqlClient sqlClient; + + public YdbKeysetPaginator(JSqlClient sqlClient) { + this.sqlClient = sqlClient; + } + + public static class Page { + private final List rows; + private final List nextCursor; + + public Page(List rows, List nextCursor) { + this.rows = rows; + this.nextCursor = nextCursor; + } + + public List getRows() { + return rows; + } + + public List getNextCursor() { + return nextCursor; + } + + public boolean hasNextPage() { + return nextCursor != null; + } + } + + public , E, R> Page fetchPage( + T table, + List> keyColumns, + List cursor, + int limit, + Function> keyExtractor, + BiFunction, T, ConfigurableRootQuery> querySetup + ) { + if (keyColumns.isEmpty()) { + throw new IllegalArgumentException("keyColumns must not be empty"); + } else if (cursor != null && cursor.size() != keyColumns.size()) { + throw new IllegalArgumentException( + "cursor has " + cursor.size() + " values, but keyColumns has " + keyColumns.size() + ); + } + + MutableRootQuery query = sqlClient.createQuery(table); + + if (cursor != null) { + query.where(buildKeysetPredicate(keyColumns, cursor)); + } + + ConfigurableRootQuery configured = querySetup.apply(query, table); + + List fetched = configured.limit(limit + 1, 0L).execute(); + + boolean hasMore = fetched.size() > limit; + List pageRows = hasMore ? new ArrayList<>(fetched.subList(0, limit)) : fetched; + + List nextCursor = hasMore + ? keyExtractor.apply(pageRows.get(pageRows.size() - 1)) + : null; + + return new Page<>(pageRows, nextCursor); + } + + private static Predicate buildKeysetPredicate( + List> keyColumns, + List cursorValues + ) { + int n = keyColumns.size(); + + StringBuilder sql = new StringBuilder("("); + + for (int i = 0; i < n; i++) { + if (i > 0) { + sql.append(", "); + } + + sql.append("%e"); + } + + sql.append(") > ("); + + for (int i = 0; i < n; i++) { + if (i > 0) { + sql.append(", "); + } + + sql.append("%v"); + } + + sql.append(")"); + + NativeBuilder.Prd builder = Predicate.sqlBuilder(sql.toString()); + for (Expression col : keyColumns) { + builder.expression(col); + } + + for (Object val : cursorValues) { + builder.value(val); + } + + return builder.build(); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YqlClientBuilder.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YqlClientBuilder.java new file mode 100644 index 00000000..75a04929 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/YqlClientBuilder.java @@ -0,0 +1,43 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.runtime.DefaultExecutor; +import org.babyfish.jimmer.sql.runtime.JSqlClientImplementor; +import ydb.jimmer.dialect.scalar.DurationProvider; +import ydb.jimmer.dialect.transaction.YqlClient; +import ydb.jimmer.dialect.transaction.YdbTxConnectionManager; + +import javax.sql.DataSource; +import java.util.function.Function; + +public final class YqlClientBuilder { + private YqlClientBuilder() {} + + public static YqlClient getYqlClient( + DataSource dataSource, + Function block + ) { + return new YqlClient((JSqlClientImplementor) buildSqlClient(dataSource, block).build()); + } + + private static JSqlClient.Builder buildSqlClient( + DataSource dataSource, + Function block + ) { + return block.apply(addScalarProviders( + JSqlClient.newBuilder() + .setDialect(new YdbDialect()) + .setConnectionManager(new YdbTxConnectionManager(dataSource)) + .setCacheOperator(new UuidTransactionCacheOperator()) + .setExecutor(new YdbExecutor(DefaultExecutor.INSTANCE)) + )); + } + + public static YqlClient getYqlClient(DataSource dataSource) { + return getYqlClient(dataSource, x -> x); + } + + public static JSqlClient.Builder addScalarProviders(JSqlClient.Builder builder) { + return builder.addScalarProvider(new DurationProvider()); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbClassMapping.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbClassMapping.java new file mode 100644 index 00000000..efa7126a --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbClassMapping.java @@ -0,0 +1,62 @@ +package ydb.jimmer.dialect.constant; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Provides mappings from Java classes + * to the YDB and JDBC data types. + */ +public final class YdbClassMapping { + private YdbClassMapping() {} + + public static final Map, String> classToYdbType; + public static final Map, Integer> classToJdbcType; + + private static final Map, String> classToYdbTypeBuilder = new HashMap<>(); + private static final Map, Integer> classToJdbcTypeBuilder = new HashMap<>(); + + static { + add("Bool", YdbJdbcTypes.BOOL, boolean.class, Boolean.class); + + add("Int8", YdbJdbcTypes.INT8, byte.class, Byte.class); + add("Int16", YdbJdbcTypes.INT16, short.class, Short.class); + add("Int32", YdbJdbcTypes.INT32, int.class, Integer.class, LocalTime.class, Time.class); + add("Int64", YdbJdbcTypes.INT64, long.class, Long.class, BigInteger.class); + add("Decimal(22, 9)", YdbJdbcTypes.DECIMAL_22_9, BigDecimal.class); + + add("Float", YdbJdbcTypes.FLOAT, float.class, Float.class); + add("Double", YdbJdbcTypes.DOUBLE, double.class, Double.class); + + add("Utf8", YdbJdbcTypes.TEXT, String.class); + add("String", YdbJdbcTypes.BYTES, byte[].class); + + add("Uuid", YdbJdbcTypes.UUID, UUID.class); + + add("Date32", YdbJdbcTypes.DATE32, Date.class, LocalDate.class); + add("Datetime64", YdbJdbcTypes.DATETIME64, LocalDateTime.class); + add("Timestamp64", YdbJdbcTypes.TIMESTAMP64, java.util.Date.class, Timestamp.class, Instant.class); + add("Interval64", YdbJdbcTypes.INTERVAL64, Duration.class); + + classToYdbType = Map.copyOf(classToYdbTypeBuilder); + classToJdbcType = Map.copyOf(classToJdbcTypeBuilder); + } + + private static void add(String ydbType, int jdbcType, Class... classes) { + for (Class clazz : classes) { + classToYdbTypeBuilder.put(clazz, ydbType); + classToJdbcTypeBuilder.put(clazz, jdbcType); + } + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbConst.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbConst.java new file mode 100644 index 00000000..00774de8 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbConst.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.constant; + +/** + * Contains constants for the SQL types used in the YDB driver. + * The values are taken from /tech/ydb/jdbc/YdbConst.java in the YDB driver. + */ +public final class YdbConst { + private YdbConst() {} + + public static final int SQL_KIND_PRIMITIVE = 10000; + public static final int SQL_KIND_DECIMAL = 1 << 14; // 16384 + + public static final int ONLINE_CONSISTENT_READ_ONLY = 16; + public static final int ONLINE_INCONSISTENT_READ_ONLY = ONLINE_CONSISTENT_READ_ONLY + 1; + public static final int STALE_READ_ONLY = 32; +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbJdbcTypes.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbJdbcTypes.java new file mode 100644 index 00000000..bb593505 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/constant/YdbJdbcTypes.java @@ -0,0 +1,140 @@ +package ydb.jimmer.dialect.constant; + +/** + * Contains constants of the SQL types used in the YDB driver. + * The values are taken from /tech/ydb/jdbc/common/YdbTypes.java in the YDB driver. + */ +public final class YdbJdbcTypes { + private YdbJdbcTypes() {} + + /** Boolean value. */ + public static final int BOOL = YdbConst.SQL_KIND_PRIMITIVE; + + /** A signed integer. Acceptable values: from -2^7 to 2^7–1. Not supported for table columns */ + public static final int INT8 = YdbConst.SQL_KIND_PRIMITIVE + 1; + + /** An unsigned integer. Acceptable values: from 0 to 2^8–1. */ + public static final int UINT8 = YdbConst.SQL_KIND_PRIMITIVE + 2; + + /** A signed integer. Acceptable values: from –2^15 to 2^15–1. Not supported for table columns */ + public static final int INT16 = YdbConst.SQL_KIND_PRIMITIVE + 3; + + /** An unsigned integer. Acceptable values: from 0 to 2^16–1. Not supported for table columns */ + public static final int UINT16 = YdbConst.SQL_KIND_PRIMITIVE + 4; + + /** A signed integer. Acceptable values: from –2^31 to 2^31–1. */ + public static final int INT32 = YdbConst.SQL_KIND_PRIMITIVE + 5; + + /** An unsigned integer. Acceptable values: from 0 to 2^32–1. */ + public static final int UINT32 = YdbConst.SQL_KIND_PRIMITIVE + 6; + + /** A signed integer. Acceptable values: from –2^63 to 2^63–1. */ + public static final int INT64 = YdbConst.SQL_KIND_PRIMITIVE + 7; + + /** An unsigned integer. Acceptable values: from 0 to 2^64–1. */ + public static final int UINT64 = YdbConst.SQL_KIND_PRIMITIVE + 8; + + /** A real number with variable precision, 4 bytes in size. Can't be used in the primary key */ + public static final int FLOAT = YdbConst.SQL_KIND_PRIMITIVE + 9; + + /** A real number with variable precision, 8 bytes in size. Can't be used in the primary key */ + public static final int DOUBLE = YdbConst.SQL_KIND_PRIMITIVE + 10; + + /** A binary data, synonym for YDB type String */ + public static final int BYTES = YdbConst.SQL_KIND_PRIMITIVE + 11; + + /** Text encoded in UTF-8, synonym for YDB type Utf8 */ + public static final int 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 */ + public static final int YSON = YdbConst.SQL_KIND_PRIMITIVE + 13; + + /** JSON represented as text. Doesn't support matching, can't be used in the primary key */ + public static final int JSON = YdbConst.SQL_KIND_PRIMITIVE + 14; + + /** Universally unique identifier UUID. Not supported for table columns */ + public static final int UUID = YdbConst.SQL_KIND_PRIMITIVE + 15; + + /** + * A moment in time corresponding to midnight in UTC, precision to the day. + *

+ * Possible values: + * from 00:00 01.01.1970 to 00:00 01.01.2106 + */ + public static final int DATE = YdbConst.SQL_KIND_PRIMITIVE + 16; + + /** + * A moment in time in UTC, precision to the second + *

+ * Possible values: + * from 00:00 01.01.1970 to 00:00 01.01.2106 + */ + public static final int DATETIME = YdbConst.SQL_KIND_PRIMITIVE + 17; + + /** + * A moment in time in UTC, precision to the microsecond + *

+ * Possible values: + * from 00:00 01.01.1970 to 00:00 01.01.2106 + */ + public static final int TIMESTAMP = YdbConst.SQL_KIND_PRIMITIVE + 18; + + /** + * Time interval, precision to the microsecond + *

+ * Possible values: + * from -136 years to +136 years + */ + public static final int INTERVAL = YdbConst.SQL_KIND_PRIMITIVE + 19; + + /** Date with time zone label, precision to the day */ + public static final int TZ_DATE = YdbConst.SQL_KIND_PRIMITIVE + 20; + + /** Date/time with time zone label, precision to the second */ + public static final int TZ_DATETIME = YdbConst.SQL_KIND_PRIMITIVE + 21; + + /** Date/time with time zone label, precision to the microsecond */ + public static final int TZ_TIMESTAMP = YdbConst.SQL_KIND_PRIMITIVE + 22; + + /** JSON in an indexed binary representation. Doesn't support matching, can't be used in the primary key */ + public static final int JSON_DOCUMENT = YdbConst.SQL_KIND_PRIMITIVE + 23; + + /** + * A moment in time corresponding to midnight in UTC, precision to the day. + *

+ * Possible values: + * from 00:00 01.01.144169 BC to 00:00 01.01.148107 AD + */ + public static final int DATE32 = YdbConst.SQL_KIND_PRIMITIVE + 25; + + /** + * A moment in time in UTC, precision to the second + *

+ * Possible values: + * from 00:00 01.01.144169 BC to 00:00 01.01.148107 AD + */ + public static final int DATETIME64 = YdbConst.SQL_KIND_PRIMITIVE + 26; + + /** + * A moment in time in UTC, precision to the microsecond + *

+ * Possible values: + * from 00:00 01.01.144169 BC to 00:00 01.01.148107 AD + */ + public static final int TIMESTAMP64 = YdbConst.SQL_KIND_PRIMITIVE + 27; + + /** + * Time interval, precision to the microsecond + *

+ * Possible values: + * from -292277 years to +292277 years + */ + public static final int INTERVAL64 = YdbConst.SQL_KIND_PRIMITIVE + 28; + + /** + * Real number with fixed precision + *

+ * Default value + */ + public static final int DECIMAL_22_9 = YdbConst.SQL_KIND_DECIMAL + (22 << 6) + 9; +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DumbYdbScalarProvider.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DumbYdbScalarProvider.java new file mode 100644 index 00000000..a814f8b8 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DumbYdbScalarProvider.java @@ -0,0 +1,53 @@ +package ydb.jimmer.dialect.scalar; + +import org.babyfish.jimmer.sql.runtime.AbstractScalarProvider; +import org.babyfish.jimmer.sql.runtime.Reader; +import org.jetbrains.annotations.NotNull; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Passes the given Java type (scalar) + * as a new Java type (SQL) without any changes. + * This class is used when the given Java type is supported by the JDBC driver, + * but is not supported by Jimmer. + * + * @param the class that needs to pass to the driver without any changes + */ +public abstract class DumbYdbScalarProvider extends AbstractScalarProvider { + private final Class clazz; + + public DumbYdbScalarProvider(Class clazz) { + super(clazz, clazz); + + this.clazz = clazz; + } + + @Override + public T toScalar(@NotNull T sqlValue) { + return sqlValue; + } + + @Override + public T toSql(@NotNull T scalarValue) { + return scalarValue; + } + + @Override + public Reader reader() { + return new DumbReader(); + } + + private class DumbReader implements Reader { + @Override + public T read(ResultSet rs, Context ctx) throws SQLException { + return rs.getObject(ctx.col(), clazz); + } + + @Override + public void skip(Context ctx) { + ctx.col(); + } + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DurationProvider.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DurationProvider.java new file mode 100644 index 00000000..b647b3a3 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/scalar/DurationProvider.java @@ -0,0 +1,9 @@ +package ydb.jimmer.dialect.scalar; + +import java.time.Duration; + +public class DurationProvider extends DumbYdbScalarProvider { + public DurationProvider() { + super(Duration.class); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/RetryConfig.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/RetryConfig.java new file mode 100644 index 00000000..f3246d09 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/RetryConfig.java @@ -0,0 +1,136 @@ +package ydb.jimmer.dialect.transaction; + +/** + * The configuration class for transaction retries. + * Most constants are taken from {@code spring-ydb-retry}'s {@code YdbRetryPolicyConfig.java} + * + * @param maxAttempts A maximum number of attempts for the transaction + * @param slowBackoffBaseMs slow backoff to wait for slow transaction errors + * @param fastBackoffBaseMs fast backoff to wait for fast transaction errors + * @param slowCapBackoffMs maximum backoff for slow transaction errors + * @param fastCapBackoffMs maximum backoff for fast transaction errors + * @param slowCeiling Ceiling on the exponent so that {@code slowBackoffBaseMs * multiplier^ceiling} + * just reaches {@code slowCapBackoffMs} + * @param fastCeiling Ceiling on the exponent so that {@code fastBackoffBaseMs * multiplier^ceiling} + * just reaches {@code fastCapBackoffMs} + * @param backoffMultiplier A multiplier for a backoff for the next retry attempt + * @param idempotent Are the operations in the transaction idempotent + */ +public record RetryConfig( + int maxAttempts, + int slowBackoffBaseMs, + int fastBackoffBaseMs, + int slowCapBackoffMs, + int fastCapBackoffMs, + int slowCeiling, + int fastCeiling, + double backoffMultiplier, + boolean idempotent +) { + public final static RetryConfig DEFAULT = new Builder().build(); + + public RetryConfig { + if (maxAttempts < 1) { + throw new IllegalArgumentException("maxAttempts must be >= 1"); + } + if (slowBackoffBaseMs < 0 + || fastBackoffBaseMs < 0 + || slowCapBackoffMs < 0 + || fastCapBackoffMs < 0) { + throw new IllegalArgumentException("backoff values must be >= 0"); + } + if (slowCeiling < 1 || fastCeiling < 1) { + throw new IllegalArgumentException("ceiling values must be >= 1"); + } + if (backoffMultiplier <= 1) { + throw new IllegalArgumentException("backoffMultiplier must be > 1"); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private int maxAttempts = 1; + private int slowBackoffBaseMs = 50; + private int fastBackoffBaseMs = 5; + private int slowCapBackoffMs = 5_000; + private int fastCapBackoffMs = 500; + private double backoffMultiplier = 2.0; + private boolean idempotent = false; + + public Builder maxAttempts(int value) { + this.maxAttempts = value; + return this; + } + + public Builder backoffBaseMs(int value) { + return this.slowBackoffBaseMs(value).fastBackoffBaseMs(value); + } + + public Builder slowBackoffBaseMs(int value) { + this.slowBackoffBaseMs = value; + return this; + } + + public Builder fastBackoffBaseMs(int value) { + this.fastBackoffBaseMs = value; + return this; + } + + public Builder capBackoffMs(int value) { + return this.slowCapBackoffMs(value).fastCapBackoffMs(value); + } + + public Builder slowCapBackoffMs(int value) { + this.slowCapBackoffMs = value; + return this; + } + + public Builder fastCapBackoffMs(int value) { + this.fastCapBackoffMs = value; + return this; + } + + public Builder backoffMultiplier(double value) { + this.backoffMultiplier = value; + return this; + } + + public Builder idempotent(boolean value) { + this.idempotent = value; + return this; + } + + public RetryConfig build() { + if (slowCapBackoffMs < 0 || fastCapBackoffMs < 0) { + throw new IllegalArgumentException("backoff cap values must be >= 0"); + } + if (backoffMultiplier < 1) { + throw new IllegalArgumentException("backoffMultiplier must be >= 1"); + } + + int slowCeiling = ceilingFromCapBackoffMs(slowCapBackoffMs, backoffMultiplier); + int fastCeiling = ceilingFromCapBackoffMs(fastCapBackoffMs, backoffMultiplier); + + return new RetryConfig( + maxAttempts, slowBackoffBaseMs, fastBackoffBaseMs, + slowCapBackoffMs, fastCapBackoffMs, slowCeiling, fastCeiling, + backoffMultiplier, idempotent); + } + + /** + * Ceiling on the exponent so that {@code baseMs * multiplier^ceiling} just reaches {@code capMs}. + * The formula is ported from {@code spring-ydb-retry}'s {@code YdbRetryPolicyConfig.java}: + * {@code ceil(ln(capMs + multiplier - 1) / ln(multiplier))}. + */ + private static int ceilingFromCapBackoffMs(int capBackoffMs, double multiplier) { + if (capBackoffMs <= 0) { + return 0; + } + double value = capBackoffMs + (multiplier - 1); + return (int) Math.ceil(Math.log(value) / Math.log(multiplier)); + } + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/TransactionContext.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/TransactionContext.java new file mode 100644 index 00000000..2e14c8da --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/TransactionContext.java @@ -0,0 +1,31 @@ +package ydb.jimmer.dialect.transaction; + +/** + * This class is used to pass transaction setting + * to the ConnectionManager. + */ +public class TransactionContext { + private static final ThreadLocal LOCAL_CONTEXT = new ThreadLocal<>(); + + public static void setSettings(int isolationLevel, boolean readOnly) { + LOCAL_CONTEXT.set(new TransactionSettings(isolationLevel, readOnly)); + } + + public static TransactionSettings getSettings() { + return LOCAL_CONTEXT.get(); + } + + public static void clear() { + LOCAL_CONTEXT.remove(); + } + + public static class TransactionSettings { + final int isolationLevel; + final boolean readOnly; + + TransactionSettings(int isolationLevel, boolean readOnly) { + this.isolationLevel = isolationLevel; + this.readOnly = readOnly; + } + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbDelayCalculator.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbDelayCalculator.java new file mode 100644 index 00000000..f69158f6 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbDelayCalculator.java @@ -0,0 +1,48 @@ +package ydb.jimmer.dialect.transaction; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Backoff math, ported from {@code spring-ydb-retry}'s {@code YdbRetryPolicy.java}. + * Added support for changing the multiplier for the backoff/delay. + * + *

{@link #calculateBackoffMillis} computes the un-jittered backoff window as + * {@code min(baseMs * 2^min(ceiling, attempt), capMs)}; {@link #fullJitterMillis} and + * {@link #equalJitterMillis} then apply jitter on top of that window. + */ +public final class YdbDelayCalculator { + private YdbDelayCalculator() {} + + /** + * Pre-jitter backoff window: {@code min(baseMs * multiplier^min(ceiling, attempt), capMs)}. + */ + public static int calculateBackoffMillis(int baseMs, int capMs, int ceiling, double multiplier, int attempt) { + int n = Math.min(ceiling, attempt); + long scaled = (long) ((long) baseMs * Math.pow(multiplier, n)); + return (int) Math.min(scaled, capMs); + } + + /** + * Full jitter: uniform in {@code [0, calculatedBackoff]}. + */ + public static long fullJitterMillis(int baseMs, int capMs, int ceiling, double multiplier, int attempt) { + int calculatedBackoff = calculateBackoffMillis(baseMs, capMs, ceiling, multiplier, attempt); + return randomLong(calculatedBackoff + 1L); + } + + /** + * Equal jitter: {@code calculatedBackoff/2 + calculatedBackoff%2 + random(0..calculatedBackoff/2)}. + */ + public static long equalJitterMillis(int baseMs, int capMs, int ceiling, double multiplier, int attempt) { + int calculatedBackoff = calculateBackoffMillis(baseMs, capMs, ceiling, multiplier, attempt); + int temp = calculatedBackoff / 2; + return temp + calculatedBackoff % 2 + randomLong(temp + 1L); + } + + private static long randomLong(long boundExclusive) { + if (boundExclusive <= 0) { + return 0L; + } + return ThreadLocalRandom.current().nextLong(boundExclusive); + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbRetryPolicy.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbRetryPolicy.java new file mode 100644 index 00000000..f6f4d172 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbRetryPolicy.java @@ -0,0 +1,110 @@ +package ydb.jimmer.dialect.transaction; + +import java.sql.SQLException; +import java.util.OptionalLong; +import java.util.Set; + +/** + * Decides whether a YDB error should be retried and how long to wait before the next attempt. + * Logic is ported one-to-one from {@code spring-ydb-retry}'s {@code YdbRetryPolicy.kt}. + * + *

    + *
  • {@link YdbDelayCalculator#fullJitterMillis} — {@code Aborted}, + * {@code Undetermined}
  • + *
  • {@link YdbDelayCalculator#equalJitterMillis} on the fast tier — + * {@code Unavailable}, transport errors, {@code CLIENT_GRPC_ERROR}
  • + *
  • {@link YdbDelayCalculator#equalJitterMillis} on the slow tier — + * {@code Overloaded}, {@code CLIENT_RESOURCE_EXHAUSTED}
  • + *
  • zero delay — {@code BadSession}, {@code SessionBusy}, {@code SessionExpired}
  • + *
+ * + *

Not retried here (add handling if your workload needs it): {@code TIMEOUT}, + * {@code CLIENT_DEADLINE_EXPIRED}, {@code PRECONDITION_FAILED}, and other vendor codes. + * Setting {@code idempotent = true} retries any code returned by this policy, not only + * {@link #isTransientVendorCode}. {@code SESSION_EXPIRED} is retried with zero backoff only when + * the call is marked idempotent and is not in the transient set. + */ +public final class YdbRetryPolicy { + private YdbRetryPolicy() {} + + /** + * These error codes are retried even when the call is not idempotent. + */ + public static final Set TRANSIENT_VENDOR_CODES = Set.of( + YdbVendorCode.ABORTED, + YdbVendorCode.UNAVAILABLE, + YdbVendorCode.OVERLOADED, + YdbVendorCode.BAD_SESSION, + YdbVendorCode.SESSION_BUSY, + YdbVendorCode.CLIENT_RESOURCE_EXHAUSTED + ); + + /** + * Returns {@code true} if the vendor code is in the always-retried (transient) set. + */ + public static boolean isTransientVendorCode(int vendorCode) { + return TRANSIENT_VENDOR_CODES.contains(vendorCode); + } + + /** + * Walks the cause chain of {@code error} looking for a {@link SQLException} with a non-zero + * vendor code. Mirrors kotlin-exposed's {@code extractVendorCode}. + * + * @return non-zero vendor code, or {@code 0} if no YDB-style code was found + */ + public static int extractVendorCode(Throwable error) { + Throwable current = error; + while (current != null) { + if (current instanceof SQLException sqlException) { + int vendorCode = sqlException.getErrorCode(); + if (vendorCode != 0) { + return vendorCode; + } + } + current = current.getCause(); + } + return 0; + } + + public static OptionalLong getNextRetryDelayMs(int vendorCode, int attempt, RetryConfig config) { + // {@code attempt} is the zero-based index of the attempt that has just failed, so the next + // attempt is allowed only while we stay within the total {@code maxAttempts} budget + // (which counts the initial execution). + if (attempt + 1 >= config.maxAttempts()) { + return OptionalLong.empty(); + } + if (vendorCode == 0) { + return OptionalLong.empty(); + } + if (!config.idempotent() && !isTransientVendorCode(vendorCode)) { + return OptionalLong.empty(); + } + + return switch (vendorCode) { + case YdbVendorCode.BAD_SESSION, YdbVendorCode.SESSION_BUSY, YdbVendorCode.SESSION_EXPIRED -> + OptionalLong.of(0L); + case YdbVendorCode.ABORTED, YdbVendorCode.UNDETERMINED -> + OptionalLong.of(YdbDelayCalculator.fullJitterMillis( + config.fastBackoffBaseMs(), + config.fastCapBackoffMs(), + config.fastCeiling(), + config.backoffMultiplier(), + attempt)); + case YdbVendorCode.UNAVAILABLE, YdbVendorCode.TRANSPORT_UNAVAILABLE, YdbVendorCode.CLIENT_GRPC_ERROR -> + OptionalLong.of(YdbDelayCalculator.equalJitterMillis( + config.fastBackoffBaseMs(), + config.fastCapBackoffMs(), + config.fastCeiling(), + config.backoffMultiplier(), + attempt)); + case YdbVendorCode.OVERLOADED, YdbVendorCode.CLIENT_RESOURCE_EXHAUSTED -> + OptionalLong.of(YdbDelayCalculator.equalJitterMillis( + config.slowBackoffBaseMs(), + config.slowCapBackoffMs(), + config.slowCeiling(), + config.backoffMultiplier(), + attempt)); + default -> OptionalLong.empty(); + }; + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManager.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManager.java new file mode 100644 index 00000000..10e6d491 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManager.java @@ -0,0 +1,235 @@ +package ydb.jimmer.dialect.transaction; + +import org.babyfish.jimmer.sql.exception.ExecutionException; +import org.babyfish.jimmer.sql.transaction.Propagation; +import org.babyfish.jimmer.sql.transaction.TxConnectionManager; +import org.jetbrains.annotations.Nullable; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.OptionalLong; +import java.util.function.Function; + +/** + * Provides propagation, isolation level, read only mode + * and retry for transactions. + */ +public class YdbTxConnectionManager implements TxConnectionManager { + private final DataSource dataSource; + private final ThreadLocal scopeLocal = new ThreadLocal<>(); + + public YdbTxConnectionManager(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public final R execute(@Nullable Connection con, Function block) { + if (con != null) { + return block.apply(con); + } + return execute(block); + } + + @Override + public final R execute(Function block) { + return executeTransaction(Propagation.SUPPORTS, block); + } + + @Override + public final R executeTransaction(Function block) { + return executeTransaction(Propagation.REQUIRED, block); + } + + public final R executeTransaction(RetryConfig config, Function block) { + return executeTransaction(config, Propagation.REQUIRES_NEW, block); + } + + @Override + public final R executeTransaction(Propagation propagation, Function block) { + return executeTransaction(RetryConfig.DEFAULT, propagation, block); + } + + public final R executeTransaction( + RetryConfig config, + Propagation propagation, + Function block + ) { + OptionalLong delay; + Throwable retryableException; + for (int attempt = 0; attempt < config.maxAttempts(); attempt++) { + try { + Scope parent = scopeLocal.get(); + Scope scope = createScope(parent, propagation); + scopeLocal.set(scope); + try { + boolean errorOccurred = false; + try { + return execute(scope.con, block); + } catch (RuntimeException | Error ex) { + errorOccurred = true; + + if (ex instanceof RuntimeException) { + delay = YdbRetryPolicy.getNextRetryDelayMs( + YdbRetryPolicy.extractVendorCode(ex), + attempt, + config + ); + if (delay.isEmpty()) { + throw ex; + } + retryableException = ex; + } else { + throw ex; + } + } finally { + scope.terminate(errorOccurred); + } + } finally { + if (parent != null) { + scopeLocal.set(parent); + } else { + scopeLocal.remove(); + } + } + } catch (SQLException ex) { + throw new ExecutionException("JDBC error raised: " + ex.getMessage(), ex); + } + + try { + Thread.sleep(delay.getAsLong()); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + ex.addSuppressed(retryableException); + throw new RuntimeException(ex); + } + } + + throw new RuntimeException("Max attempts exceeded"); + } + + protected Connection openConnection() throws SQLException { + return dataSource.getConnection(); + } + + protected void closeConnection(Connection con) throws SQLException { + con.close(); + } + + protected void startTransaction(Connection con) throws SQLException { + con.setAutoCommit(false); + + TransactionContext.TransactionSettings settings = TransactionContext.getSettings(); + if (settings != null) { + if (settings.isolationLevel != Connection.TRANSACTION_NONE) { + con.setTransactionIsolation(settings.isolationLevel); + } + con.setReadOnly(settings.readOnly); + } + } + + protected void commitTransaction(Connection con) throws SQLException { + con.commit(); + } + + protected void rollbackTransaction(Connection con) throws SQLException { + con.rollback(); + } + + protected void abortTransaction(Connection con) throws SQLException { + con.setAutoCommit(true); + } + + private Scope createScope(Scope parent, Propagation propagation) throws SQLException { + return switch (propagation) { + case REQUIRES_NEW -> new Scope(parent, false, true); + case SUPPORTS -> new Scope(parent, true, parent != null && parent.withTransaction); + case NOT_SUPPORTED -> new Scope(parent, true, false); + case MANDATORY -> { + if (parent == null || !parent.withTransaction) { + throw new ExecutionException( + "The transaction propagation is \"MANDATORY\" but there is no transaction context" + ); + } + yield new Scope(parent, true, true); + } + case NEVER -> { + if (parent != null && parent.withTransaction) { + throw new ExecutionException( + "The transaction propagation is \"NEVER\" but there is already a transaction context" + ); + } + yield new Scope(parent, true, false); + } + default -> // REQUIRED + new Scope(parent, true, true); + }; + } + + private class Scope { + private final Connection con; + + private final boolean withTransaction; + private final boolean connectionOwner; + private final boolean transactionOwner; + + Scope(Scope parent, boolean borrowConnection, boolean withTransaction) throws SQLException { + if (parent != null && parent.withTransaction && !withTransaction) { + borrowConnection = false; + } + + Connection con; + if (parent != null && borrowConnection) { + con = parent.con; + this.connectionOwner = false; + } else { + con = openConnection(); + this.connectionOwner = true; + } + + this.withTransaction = withTransaction; + if (!withTransaction) { + transactionOwner = false; + } else if (connectionOwner) { + transactionOwner = true; + } else { + transactionOwner = !parent.withTransaction; + } + + if (transactionOwner) { + try { + startTransaction(con); + } catch (SQLException | RuntimeException | Error ex) { + closeConnection(con); + this.con = null; + throw ex; + } + } + this.con = con; + } + + void terminate(boolean error) throws SQLException { + Connection con = this.con; + if (con == null) { + return; + } + + try { + if (transactionOwner) { + if (error) { + rollbackTransaction(con); + } else { + commitTransaction(con); + } + if (!connectionOwner) { + abortTransaction(con); + } + } + } finally { + if (connectionOwner) { + closeConnection(con); + } + } + } + } +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbVendorCode.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbVendorCode.java new file mode 100644 index 00000000..fd64a61c --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbVendorCode.java @@ -0,0 +1,41 @@ +package ydb.jimmer.dialect.transaction; + +/** + * YDB status codes as JDBC {@link java.sql.SQLException#getErrorCode() vendor codes}. + * + *

Values match + * {@code tech.ydb.core.StatusCode} + * and + * {@code tech.ydb.core.Constants}. + * Kept as raw {@code int} constants so this module does not need {@code ydb-sdk-core} on the + * runtime classpath. + */ +public final class YdbVendorCode { + private YdbVendorCode() {} + + public static final int SERVER_STATUSES_FIRST = 400_000; + public static final int TRANSPORT_STATUSES_FIRST = 401_000; + public static final int INTERNAL_CLIENT_FIRST = 402_000; + + public static final int ABORTED = SERVER_STATUSES_FIRST + 40; + public static final int UNAVAILABLE = SERVER_STATUSES_FIRST + 50; + public static final int OVERLOADED = SERVER_STATUSES_FIRST + 60; + public static final int TIMEOUT = SERVER_STATUSES_FIRST + 90; + public static final int BAD_SESSION = SERVER_STATUSES_FIRST + 100; + public static final int PRECONDITION_FAILED = SERVER_STATUSES_FIRST + 120; + public static final int NOT_FOUND = SERVER_STATUSES_FIRST + 140; + public static final int SESSION_EXPIRED = SERVER_STATUSES_FIRST + 150; + public static final int UNDETERMINED = SERVER_STATUSES_FIRST + 170; + public static final int SESSION_BUSY = SERVER_STATUSES_FIRST + 190; + + public static final int TRANSPORT_UNAVAILABLE = TRANSPORT_STATUSES_FIRST + 10; + public static final int CLIENT_RESOURCE_EXHAUSTED = TRANSPORT_STATUSES_FIRST + 20; + public static final int CLIENT_DEADLINE_EXCEEDED = TRANSPORT_STATUSES_FIRST + 30; + public static final int CLIENT_INTERNAL_ERROR = TRANSPORT_STATUSES_FIRST + 50; + public static final int CLIENT_CANCELLED = TRANSPORT_STATUSES_FIRST + 60; + + public static final int CLIENT_DISCOVERY_FAILED = INTERNAL_CLIENT_FIRST + 10; + public static final int CLIENT_LIMITS_REACHED = INTERNAL_CLIENT_FIRST + 20; + public static final int CLIENT_DEADLINE_EXPIRED = INTERNAL_CLIENT_FIRST + 30; + public static final int CLIENT_GRPC_ERROR = INTERNAL_CLIENT_FIRST + 40; +} diff --git a/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YqlClient.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YqlClient.java new file mode 100644 index 00000000..f280b30f --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YqlClient.java @@ -0,0 +1,99 @@ +package ydb.jimmer.dialect.transaction; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.di.AbstractJSqlClientDelegate; +import org.babyfish.jimmer.sql.runtime.JSqlClientImplementor; +import ydb.jimmer.dialect.constant.YdbConst; + +import java.sql.Connection; +import java.util.function.Supplier; + +/** + * Provides methods for setting transaction isolation level + * to the {@link JSqlClient}. + */ +public class YqlClient extends AbstractJSqlClientDelegate { + private final JSqlClientImplementor delegate; + + public YqlClient(JSqlClientImplementor delegate) { + this.delegate = delegate; + } + + @Override + protected JSqlClientImplementor sqlClient() { + return delegate; + } + + public R transaction(RetryConfig config, Supplier block) { + if (config.maxAttempts() == 1) { + return transaction(block); + } + + if (getConnectionManager() instanceof YdbTxConnectionManager ydbCM) { + return ydbCM.executeTransaction(config, con -> block.get()); + } + + throw new IllegalStateException( + "The connection manager does not support retries for transactions. " + + "Use YdbTxConnectionManager." + ); + } + + public R withIsolation( + RetryConfig config, + int isolationLevel, + boolean readOnly, + Supplier block + ) { + try { + TransactionContext.setSettings(isolationLevel, readOnly); + return transaction(config, block); + } finally { + TransactionContext.clear(); + } + } + + public R withIsolation(int isolationLevel, boolean readOnly, Supplier block) { + return withIsolation(RetryConfig.DEFAULT, isolationLevel, readOnly, block); + } + + public R serializableReadWrite(RetryConfig config, Supplier block) { + return withIsolation(config, Connection.TRANSACTION_SERIALIZABLE, false, block); + } + + public R serializableReadWrite(Supplier block) { + return serializableReadWrite(RetryConfig.DEFAULT, block); + } + + public R snapshotReadOnly(RetryConfig config, Supplier block) { + return withIsolation(config, Connection.TRANSACTION_SERIALIZABLE, true, block); + } + + public R snapshotReadOnly(Supplier block) { + return snapshotReadOnly(RetryConfig.DEFAULT, block); + } + + public R staleReadOnly(RetryConfig config, Supplier block) { + return withIsolation(config, YdbConst.STALE_READ_ONLY, true, block); + } + + public R staleReadOnly(Supplier block) { + return staleReadOnly(RetryConfig.DEFAULT, block); + } + + public R onlineConsistentReadOnly(RetryConfig config, Supplier block) { + return withIsolation(config, YdbConst.ONLINE_CONSISTENT_READ_ONLY, true, block); + } + + public R onlineConsistentReadOnly(Supplier block) { + return onlineConsistentReadOnly(RetryConfig.DEFAULT, block); + } + + public R onlineInconsistentReadOnly(RetryConfig config, Supplier block) { + return withIsolation(config, YdbConst.ONLINE_INCONSISTENT_READ_ONLY, true, block); + } + + public R onlineInconsistentReadOnly(Supplier block) { + return onlineInconsistentReadOnly(RetryConfig.DEFAULT, block); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractInsertTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractInsertTest.java new file mode 100644 index 00000000..bbae2c97 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractInsertTest.java @@ -0,0 +1,31 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.ast.Executable; +import org.babyfish.jimmer.sql.ast.mutation.MutationResult; +import org.junit.jupiter.api.Assertions; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.function.Consumer; + +public abstract class AbstractInsertTest extends AbstractTest { + protected void executeAndExpect(Executable query, Consumer block) { + MutationResult result = null; + Throwable throwable = null; + try (Connection connection = DriverManager.getConnection(getJdbcURL())) { + connection.setAutoCommit(false); + try { + result = query.execute(connection); + } catch (Throwable ex) { + throwable = ex; + } finally { + connection.rollback(); + } + } catch (SQLException e) { + Assertions.fail("Database threw an exception: " + e.getMessage()); + } + + block.accept(new QueryTestContext(executor.getLogs(), result, throwable)); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractSelectTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractSelectTest.java new file mode 100644 index 00000000..849a0c33 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractSelectTest.java @@ -0,0 +1,61 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.ast.Executable; +import org.junit.jupiter.api.Assertions; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import java.util.function.Consumer; + +public abstract class AbstractSelectTest extends AbstractTest { + protected static void executeAndExpect(Executable> query, Consumer block) { + List rows = null; + try (Connection connection = DriverManager.getConnection(getJdbcURL())) { + connection.setAutoCommit(false); + try { + rows = query.execute(connection); + } finally { + connection.rollback(); + } + } catch (SQLException e) { + Assertions.fail("Database threw an exception: " + e.getMessage()); + } + + block.accept(new QueryTestContext(executor.getLogs(), rows)); + } + + protected static void insert(String tableName, String... values) { + if (values.length == 0) { + return; + } + StringBuilder sql = new StringBuilder("INSERT INTO " + tableName + " (id, value) VALUES "); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sql.append(", "); + } + sql.append("(").append(i).append(", ").append(values[i]).append(")"); + } + executeSql(sql.toString()); + } + + protected static String buildJsonResponse(String[] expectedValues) { + return buildJsonResponse(0, expectedValues); + } + + protected static String buildJsonResponse(int startingId, String[] expectedValues) { + StringBuilder json = new StringBuilder("["); + for (int i = 0; i < expectedValues.length; i++) { + if (json.length() != 1) { + json.append(","); + } + json.append("{"); + json.append("\"id\":").append(startingId + i).append(",\"value\":").append(expectedValues[i]); + json.append("}"); + } + json.append("]"); + + return json.toString(); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractTest.java new file mode 100644 index 00000000..4a3737c8 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/AbstractTest.java @@ -0,0 +1,139 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.runtime.DefaultExecutor; +import org.babyfish.jimmer.sql.runtime.JSqlClientImplementor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.jdbc.datasource.init.ScriptException; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import tech.ydb.test.junit5.YdbHelperExtension; +import ydb.jimmer.dialect.scalar.DurationProvider; +import ydb.jimmer.dialect.sqlMonitor.YdbExecutorMonitor; +import ydb.jimmer.dialect.transaction.YqlClient; +import ydb.jimmer.dialect.transaction.YdbTxConnectionManager; + +import javax.sql.DataSource; +import java.net.URL; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public abstract class AbstractTest { + @RegisterExtension + private static final YdbHelperExtension ydb = new YdbHelperExtension(); + + protected static final YdbExecutorMonitor executor = new YdbExecutorMonitor( + new YdbExecutor(DefaultExecutor.INSTANCE) + ); + private static final JSqlClient yqlClient; + private static final JSqlClient yqlClientForBatch; + + static { + yqlClient = JSqlClient.newBuilder() + .setDialect(new YdbDialect()) + .setExecutor(executor) + .addScalarProvider(new DurationProvider()) + .build(); + + yqlClientForBatch = JSqlClient.newBuilder() + .setDialect(new YdbDialect()) + .setExecutor(executor) + .setExplicitBatchEnabled(true) + .setDumbBatchAcceptable(true) + .build(); + } + + protected static JSqlClient getYqlClient() { + return yqlClient; + } + + protected static JSqlClient getYqlClientForBatch() { + return yqlClientForBatch; + } + + protected static YqlClient getIsolationClient() { + DataSource dataSource = new DriverManagerDataSource(getJdbcURL()); + return new YqlClient( + (JSqlClientImplementor) JSqlClient.newBuilder() + .setConnectionManager(new YdbTxConnectionManager(dataSource)) + .setDialect(new YdbDialect()) + .setExecutor(executor) + .build()); + } + + protected void initDatabase() { + try (Connection connection = DriverManager.getConnection(getJdbcURL())) { + URL dropTablesUrl = AbstractTest.class.getClassLoader().getResource("database-drop-tables-ydb.sql"); + if (dropTablesUrl == null) { + throw new IllegalStateException("Cannot load 'database-drop-tables-ydb.sql'"); + } + try { + executeYqlScript(connection, dropTablesUrl); + } catch (ScriptException e) { + // + } + + URL url = AbstractTest.class.getClassLoader().getResource("database-ydb.sql"); + if (url == null) { + throw new IllegalStateException("Cannot load 'database-ydb.sql'"); + } + executeYqlScript(connection, url); + } catch (SQLException e) { + Assertions.fail("Database threw an exception: " + e.getMessage()); + } + } + + private void executeYqlScript(Connection connection, URL url) throws ScriptException { + ScriptUtils.executeSqlScript( + connection, + new EncodedResource(new UrlResource(url)), + false, + false, + "--", + ";", + "/*", + "*/"); + } + + protected static String getJdbcURL() { + StringBuilder jdbc = new StringBuilder("jdbc:ydb:") + .append(ydb.useTls() ? "grpcs://" : "grpc://") + .append(ydb.endpoint()) + .append("/") + .append(ydb.database()); + + if (ydb.authToken() != null) { + jdbc.append("?").append("token=").append(ydb.authToken()); + } + + return jdbc.toString(); + } + + protected static void createTable(String tableName, String typeName) { + executeSql( + "CREATE TABLE " + tableName + "(" + + "id Int8," + + "value " + typeName + "," + + "PRIMARY KEY (id)" + + ")"); + } + + protected static void dropTable(String tableName) { + executeSql("DROP TABLE " + tableName); + } + + protected static void executeSql(String sql) { + try (Connection connection = DriverManager.getConnection(getJdbcURL())) { + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } catch (SQLException e) { + Assertions.fail("Database threw an exception: " + e.getMessage()); + } + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/JoinTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/JoinTest.java new file mode 100644 index 00000000..a42b2b4a --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/JoinTest.java @@ -0,0 +1,49 @@ +package ydb.jimmer.dialect; + +import org.babyfish.jimmer.sql.JoinType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.model.StudentTable; + +public class JoinTest extends AbstractSelectTest { + @BeforeEach + protected void setup() { + initDatabase(); + } + + private void joinTest(JoinType joinType, String join) { + StudentTable table = StudentTable.$; + executeAndExpect( + getYqlClient() + .createQuery(table) + .orderBy(table.group(joinType).name().asc()) + .select(table), + cxt -> cxt.sql( + "select tb_1_.id, tb_1_.name, tb_1_.group " + + "from student tb_1_ " + + join + " join group tb_2_ on tb_1_.group = tb_2_.id " + + "order by tb_2_.name asc" + ) + ); + } + + @Test + public void leftJoinTest() { + joinTest(JoinType.LEFT, "left"); + } + + @Test + public void rightJoinTest() { + joinTest(JoinType.RIGHT, "right"); + } + + @Test + public void innerJoinTest() { + joinTest(JoinType.INNER, "inner"); + } + + @Test + public void fullJoinTest() { + joinTest(JoinType.FULL, "full"); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/QueryTestContext.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/QueryTestContext.java new file mode 100644 index 00000000..e97cdf8c --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/QueryTestContext.java @@ -0,0 +1,112 @@ +package ydb.jimmer.dialect; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.babyfish.jimmer.jackson.ImmutableModule; +import org.babyfish.jimmer.sql.ast.mutation.MutationResult; +import org.junit.jupiter.api.Assertions; +import ydb.jimmer.dialect.sqlMonitor.QueryLog; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class QueryTestContext { + private static final ObjectMapper MAPPER = new ObjectMapper() + .registerModule(new ImmutableModule()) + .registerModule(new JavaTimeModule()); + + static { + MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + } + + private final List logs; + private final List rows; + private final MutationResult result; + private final Throwable throwable; + + private int index = 0; + + public QueryTestContext(List logs, List rows) { + this.logs = logs; + this.rows = rows; + result = null; + throwable = null; + } + + public QueryTestContext(List logs, MutationResult result, Throwable throwable) { + this.logs = logs; + this.rows = new ArrayList<>(); + this.result = result; + this.throwable = throwable; + } + + public void nextStatement() { + index++; + } + + public void sql(String sql) { + Assertions.assertEquals( + sql.replace("--->", "").toLowerCase(), + logs.get(index).getSql().toLowerCase(), + "statements[" + index + "].sql"); + } + + public void variables(Object... values) { + batchVariables(0, values); + batches(1); + } + + public void batches(int batchCount) { + Assertions.assertEquals(batchCount, logs.get(index).getVariablesList().size()); + } + + public void batchVariables(int batchIndex, Object... values) { + Assertions.assertEquals( + values.length, + logs.get(index).getVariablesList().get(batchIndex).size(), + "statements[" + index + "].variables.size is error, actual variables: " + + logs.get(index).getVariablesList().get(batchIndex) + ); + for (int i = 0; i < values.length; i++) { + Object expect = values[i]; + + Object actual = logs.get(index).getVariablesList().get(batchIndex).get(i); + + Assertions.assertEquals( + expect, + actual, + "statements[" + index + "].batch[" + batchIndex + "].variables[" + i + "] is error, " + + "expected variable: " + + expect + + ", actual variable: " + + actual + ); + } + } + + public void rows(String json) { + try { + Assertions.assertEquals( + json.replace("--->", ""), + MAPPER.writeValueAsString(rows) + ); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public void error(String message) { + if (throwable == null && message == null) { + return; + } + + if (throwable == null) { + Assertions.fail("The query didn't produce an exception: " + message); + } + + Assertions.assertEquals(message, throwable.getMessage()); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/SelectTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/SelectTest.java new file mode 100644 index 00000000..cb43b015 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/SelectTest.java @@ -0,0 +1,23 @@ +package ydb.jimmer.dialect; + +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.model.StudentTable; + +public class SelectTest extends AbstractSelectTest { + @Test + public void OneEntityTest() { + initDatabase(); + + StudentTable table = StudentTable.$; + + executeAndExpect( + getYqlClient() + .createQuery(table) + .select(table), + cxt -> cxt.sql( + "select tb_1_.id, tb_1_.name, tb_1_.group " + + "from student tb_1_" + ) + ); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/batch/BatchTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/batch/BatchTest.java new file mode 100644 index 00000000..28ce64b1 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/batch/BatchTest.java @@ -0,0 +1,81 @@ +package ydb.jimmer.dialect.batch; + +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractInsertTest; +import ydb.jimmer.dialect.model.Entity; +import ydb.jimmer.dialect.model.EntityDraft; + +import java.util.ArrayList; +import java.util.List; + +public class BatchTest extends AbstractInsertTest { + private static final String TABLE_NAME = "simple_table"; + private static final String TYPE_NAME = "Int32"; + + private void batchTest( + SaveMode saveMode, + String sql, + int[][] batches, + int[][] expectedBatches + ) { + List entities = new ArrayList<>(); + for (int[] batch : batches) { + entities.add(EntityDraft.$.produce(t -> { + t.setId(batch[0]); + t.setValue(batch[1]); + })); + } + + executeAndExpect( + getYqlClientForBatch() + .getEntities() + .saveEntitiesCommand(entities) + .setMode(saveMode), + cxt -> { + cxt.sql(sql); + + for (int i = 0; i < batches.length; i++) { + cxt.batchVariables(i, expectedBatches[i][0], expectedBatches[i][1]); + } + } + ); + } + + @BeforeEach + void setup() { + createTable(TABLE_NAME, TYPE_NAME); + } + + @AfterEach + void teardown() { + dropTable(TABLE_NAME); + } + + @Test + public void insertTest() { + int[][] batches = new int[][]{{0, 123}, {1, 456}}; + batchTest(SaveMode.INSERT_ONLY, + "insert into " + TABLE_NAME + "(id, value) values(?, ?)", + batches, batches); + } + + @Test + public void updateTest() { + int[][] batches = new int[][]{{0, 123}, {1, 456}}; + int[][] expectedBatches = new int[][]{{123, 0}, {456, 1}}; + batchTest(SaveMode.UPDATE_ONLY, + "update " + TABLE_NAME + " set value = ? where id = ? returning id", + batches, expectedBatches); + } + + @Test + public void upsertTest() { + int[][] batches = new int[][]{{0, 123}, {1, 456}}; + batchTest(SaveMode.UPSERT, + "upsert into " + TABLE_NAME + "(id, value) values(?, ?)", + batches, batches); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/CrudBenchmark.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/CrudBenchmark.java new file mode 100644 index 00000000..7442774b --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/CrudBenchmark.java @@ -0,0 +1,202 @@ +package ydb.jimmer.dialect.benchmark; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.runtime.ConnectionManager; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.testcontainers.containers.FixedHostPortGenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import tech.ydb.core.grpc.GrpcTransport; +import tech.ydb.table.TableClient; +import ydb.jimmer.dialect.YdbDialect; +import ydb.jimmer.dialect.YqlClientBuilder; +import ydb.jimmer.dialect.model.EntityUuid; +import ydb.jimmer.dialect.model.EntityUuidDraft; +import ydb.jimmer.dialect.model.EntityUuidTable; +import ydb.jimmer.dialect.transaction.YqlClient; + +import javax.sql.DataSource; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 10) +@Measurement(iterations = 10, time = 15) +@Fork(1) +public class CrudBenchmark { + private static final String YDB_IMAGE = "cr.yandex/yc/yandex-docker-local-ydb:latest"; + private static final int GRPC_PORT = 2136; + private static final int MON_PORT = 8765; + private static final String TABLE = "benchmark_items"; + private static final String DDL = """ + CREATE TABLE %s ( + id Uuid, + value String, + PRIMARY KEY (id) + );""".formatted(TABLE); + + @Param({"10", "20", "50", "100", "200", "500", "1000", "10000"}) + public int rowCount; + + private FixedHostPortGenericContainer ydbContainer; + private GrpcTransport transport; + private TableClient tableClient; + private DataSource dataSource; + + private List seedIds; + private List newEntities; + + private static JSqlClient sqlClient; + private static YqlClient yqlClient; + + @Setup(Level.Trial) + public void startInfrastructure() throws Exception { + ydbContainer = new FixedHostPortGenericContainer<>(YDB_IMAGE) + .withFixedExposedPort(GRPC_PORT, GRPC_PORT) + .withExposedPorts(MON_PORT) + .withEnv("YDB_USE_IN_MEMORY_PDISKS", "true") + .withCreateContainerCmdModifier(cmd -> cmd.withHostName("localhost")) + .waitingFor( + Wait.forHttp("/") + .forPort(MON_PORT) + .forStatusCodeMatching(code -> code < 500) + .withStartupTimeout(Duration.ofMinutes(2)) + ); + ydbContainer.start(); + + String endpoint = "grpc://localhost:" + GRPC_PORT; + String database = "/local"; + + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setDriverClassName("tech.ydb.jdbc.YdbDriver"); + ds.setUrl("jdbc:ydb:" + endpoint + database); + dataSource = ds; + sqlClient = JSqlClient.newBuilder() + .setDialect(new YdbDialect() { + @Override + public String transCacheOperatorTableDDL() { + throw new UnsupportedOperationException(); + } + }) + .setConnectionManager(ConnectionManager.simpleConnectionManager(dataSource)) + .build(); + yqlClient = YqlClientBuilder.getYqlClient(dataSource); + + transport = GrpcTransport.forEndpoint(endpoint, database).build(); + tableClient = TableClient.newClient(transport).build(); + + executeSchemeQuery(DDL); + } + + private void executeSchemeQuery(String yql) { + try (var session = tableClient + .createSession(Duration.ofSeconds(10)) + .join().getValue()) { + session.executeSchemeQuery(yql) + .join().expectSuccess("DDL failed: " + yql); + } + } + + @Setup(Level.Invocation) + public void insertSeedRows() { + EntityUuidTable table = EntityUuidTable.$; + + yqlClient.createDelete(table).execute(); + + seedIds = new ArrayList<>(); + List seedEntities = new ArrayList<>(); + newEntities = new ArrayList<>(); + for (int i = 0; i < rowCount; i++) { + UUID id = UUID.randomUUID(); + seedIds.add(id); + seedEntities.add(EntityUuidDraft.$.produce(entity -> { + entity.setId(id); + entity.setValue("entity-" + id); + })); + + UUID newId = UUID.randomUUID(); + newEntities.add(EntityUuidDraft.$.produce(entity -> { + entity.setId(newId); + entity.setValue("entity-" + newId); + })); + } + + yqlClient.getEntities() + .saveEntitiesCommand(seedEntities) + .execute(); + } + + @TearDown(Level.Trial) + public void stopInfrastructure() throws Exception { + if (dataSource instanceof AutoCloseable ac) ac.close(); + if (tableClient != null) tableClient.close(); + if (transport != null) transport.close(); + if (ydbContainer != null) ydbContainer.stop(); + } + + @Benchmark + public void readYdb() { + yqlClient.getEntities().findAll(EntityUuid.class); + } + + @Benchmark + public void readJimmer() { + sqlClient.getEntities().findAll(EntityUuid.class); + } + + @Benchmark + public void saveYdb() { + yqlClient.getEntities() + .saveEntitiesCommand(newEntities) + .execute(); + } + + @Benchmark + public void saveJimmer() { + sqlClient.getEntities() + .saveEntitiesCommand(newEntities) + .execute(); + } + + @Benchmark + public void deleteYdb() { + yqlClient.getEntities().deleteAll(EntityUuid.class, seedIds); + } + + @Benchmark + public void deleteJimmer() { + yqlClient.getEntities().deleteAll(EntityUuid.class, seedIds); + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(CrudBenchmark.class.getSimpleName()) + .resultFormat(ResultFormatType.JSON) + .result("jmh-crud-results.json") + .shouldFailOnError(true) + .build(); + new Runner(opt).run(); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/AlwaysFail.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/AlwaysFail.java new file mode 100644 index 00000000..c3a8da8f --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/AlwaysFail.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; +import ydb.jimmer.dialect.transaction.YdbVendorCode; + +import java.sql.SQLException; +import java.util.function.Supplier; + +public class AlwaysFail implements Supplier { + @Override + public R get() { + throw new ExecutionException( + "Persistent failure", + new SQLException("", "", YdbVendorCode.UNAVAILABLE) + ); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/FailFirstN.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/FailFirstN.java new file mode 100644 index 00000000..255352f9 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/FailFirstN.java @@ -0,0 +1,40 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; +import ydb.jimmer.dialect.transaction.YdbVendorCode; + +import java.sql.SQLException; +import java.util.function.Supplier; + +public class FailFirstN implements Supplier { + private final int n; + private final R result; + + private int attempt; + + public FailFirstN(int n) { + this.n = n; + this.result = null; + } + + public FailFirstN(int n, R result) { + this.n = n; + this.result = result; + } + + public int getAttempt() { + return attempt; + } + + @Override + public R get() { + if (attempt++ < n) { + throw new ExecutionException( + "Expected failure number " + attempt, + new SQLException("", "", YdbVendorCode.UNAVAILABLE) + ); + } + + return result; + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/NotRetryableFail.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/NotRetryableFail.java new file mode 100644 index 00000000..c7e99447 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/NotRetryableFail.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; + +import java.sql.SQLException; +import java.util.function.Supplier; + +public class NotRetryableFail implements Supplier { + private int attempt; + + public int getAttempt() { + return attempt; + } + + @Override + public R get() { + attempt++; + throw new ExecutionException("Not retryable failure", new SQLException()); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/RetryTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/RetryTest.java new file mode 100644 index 00000000..6bb05d9c --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/RetryTest.java @@ -0,0 +1,83 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.transaction.RetryConfig; + +public class RetryTest extends AbstractSelectTest { + @Test + public void succeedAfter1Failure() { + int maxAttempts = 2; + + FailFirstN chaosPolicy = new FailFirstN<>(maxAttempts - 1); + Assertions.assertDoesNotThrow(() -> + getIsolationClient().transaction( + RetryConfig.builder() + .maxAttempts(maxAttempts) + .backoffBaseMs(0) + .build(), + chaosPolicy + ) + ); + + Assertions.assertEquals(maxAttempts, chaosPolicy.getAttempt()); + } + + @Test + public void failAllAttempts() { + int maxAttempts = 3; + + Assertions.assertThrows( + ExecutionException.class, () -> + getIsolationClient().transaction( + RetryConfig.builder() + .maxAttempts(maxAttempts) + .backoffBaseMs(0) + .build(), + new AlwaysFail<>() + ) + ); + } + + @Test + public void failWithoutRetries() { + int maxAttempts = 3; + + NotRetryableFail chaosPolicy = new NotRetryableFail<>(); + Assertions.assertThrows( + ExecutionException.class, () -> + getIsolationClient().transaction( + RetryConfig.builder() + .maxAttempts(maxAttempts) + .backoffBaseMs(0) + .build(), + chaosPolicy + ) + ); + + Assertions.assertEquals(1, chaosPolicy.getAttempt()); + } + + @Test + public void exponentialBackoffTest() { + int maxAttempts = 3; + + FailFirstN chaosPolicy = new FailFirstN<>(maxAttempts - 1); + + long start = System.currentTimeMillis(); + Assertions.assertDoesNotThrow(() -> + getIsolationClient().transaction( + RetryConfig.builder() + .maxAttempts(maxAttempts) + .backoffBaseMs(100) + .build(), + chaosPolicy + ) + ); + long elapsed = System.currentTimeMillis() - start; + + Assertions.assertTrue(elapsed >= 150, "the backoff time is less than 150ms"); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/HandleDateTimeTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/HandleDateTimeTest.java new file mode 100644 index 00000000..23533ff5 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/HandleDateTimeTest.java @@ -0,0 +1,50 @@ +package ydb.jimmer.dialect.dataTypeTest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; + +/** + * Tests if the date/time mapping integrated in Jimmer affects + * the value of the date/time objects. The changes in the values are compared + * after being put through the ydb driver mapping: + * 1. date/time -> driver + * 2. date/time -> Jimmer -> driver + */ +public class HandleDateTimeTest { + @Test + public void javaClassMappingTest() { + Instant instant = Instant.now(); + Assertions.assertEquals(instant, Timestamp.from(instant).toInstant()); + + LocalDateTime localDateTime = LocalDateTime.parse("1970-01-01T00:00:00"); + Assertions.assertEquals(localDateTime, + Instant.ofEpochMilli( + Timestamp.from((localDateTime).atZone(ZoneId.systemDefault()).toInstant()) + .getTime() + ).atZone(ZoneId.systemDefault()).toLocalDateTime() + ); + + LocalDate localDate = LocalDate.parse("1970-01-01"); + Assertions.assertEquals(localDate, + Instant.ofEpochMilli( + java.sql.Date.valueOf(localDate) + .getTime() + ).atZone(ZoneId.systemDefault()).toLocalDate() + ); + + // can't find how LocalTime is processed inside the driver +// LocalTime localTime = LocalTime.parse("10:15"); +// Assertions.assertEquals(localTime, java.sql.Time.valueOf(localTime)); + + java.util.Date utilDate = new java.util.Date(0); + Assertions.assertEquals(utilDate, new Timestamp((utilDate).getTime())); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertDataTypeTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertDataTypeTest.java new file mode 100644 index 00000000..1c1eb112 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertDataTypeTest.java @@ -0,0 +1,306 @@ +package ydb.jimmer.dialect.dataTypeTest; + +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractInsertTest; +import ydb.jimmer.dialect.model.type.ydbBool.YdbBooleanClassDraft; +import ydb.jimmer.dialect.model.type.ydbBool.YdbBooleanDraft; +import ydb.jimmer.dialect.model.type.ydbDate32.YdbDateDraft; +import ydb.jimmer.dialect.model.type.ydbDecimal.YdbBigDecimalDraft; +import ydb.jimmer.dialect.model.type.ydbDouble.YdbDoubleClassDraft; +import ydb.jimmer.dialect.model.type.ydbDouble.YdbDoubleDraft; +import ydb.jimmer.dialect.model.type.ydbEnum.Value; +import ydb.jimmer.dialect.model.type.ydbEnum.YdbEnumDraft; +import ydb.jimmer.dialect.model.type.ydbFloat.YdbFloatClassDraft; +import ydb.jimmer.dialect.model.type.ydbFloat.YdbFloatDraft; +import ydb.jimmer.dialect.model.type.ydbInt16.YdbShortClassDraft; +import ydb.jimmer.dialect.model.type.ydbInt16.YdbShortDraft; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbIntDraft; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbIntegerDraft; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbTimeDraft; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbBigIntegerDraft; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbLongClassDraft; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbLongDraft; +import ydb.jimmer.dialect.model.type.ydbInt8.YdbByteClassDraft; +import ydb.jimmer.dialect.model.type.ydbInt8.YdbByteDraft; +import ydb.jimmer.dialect.model.type.ydbInterval64.YdbDurationDraft; +import ydb.jimmer.dialect.model.type.ydbString.YdbByteArrayDraft; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbTimestampDraft; +import ydb.jimmer.dialect.model.type.ydbUtf8.YdbStringDraft; +import ydb.jimmer.dialect.model.type.ydbUuid.YdbUuidDraft; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Duration; +import java.util.UUID; + +public class InsertDataTypeTest extends AbstractInsertTest { + private void typeTest(String tableName, + String typeName, + Object input, + Object[] variables) { + createTable(tableName, typeName); + + executeAndExpect( + getYqlClient().getEntities().saveCommand(input) + .setMode(SaveMode.INSERT_ONLY), + cxt -> { + cxt.sql("insert into " + tableName + "(id, value) values(?, ?)"); + cxt.variables(variables); + } + ); + + dropTable(tableName); + } + + @Test + public void boolTest() { + Object[] variables = new Object[]{0, true}; + + typeTest("ydb_boolean", "Bool", + YdbBooleanDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Boolean) variables[1]); + }), + variables + ); + + typeTest("ydb_boolean_class", "Bool", + YdbBooleanClassDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Boolean) variables[1]); + }), + variables); + } + + @Test + public void date32Test() { + Object[] variables1 = new Object[]{0, new Date(0)}; + + typeTest("ydb_date", "Date32", + YdbDateDraft.$.produce(t -> { + t.setId((Integer) variables1[0]); + t.setValue((Date) variables1[1]); + }), + variables1); + } + + @Test + public void decimalTest() { + Object[] variables = new Object[]{0, new BigDecimal(0)}; + + typeTest("ydb_big_decimal", "Decimal(22, 9)", + YdbBigDecimalDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((BigDecimal) variables[1]); + }), + variables); + } + + @Test + public void doubleTest() { + Object[] variables = new Object[]{0, 0.1}; + + typeTest("ydb_double", "Double", + YdbDoubleDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((double) variables[1]); + }), + variables); + + typeTest("ydb_double_class", "Double", + YdbDoubleClassDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Double) variables[1]); + }), + variables); + } + + @Test + public void enumTest() { + Object[] variables = new Object[]{0, Value.ONE}; + + typeTest("ydb_enum", "Utf8", + YdbEnumDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Value) variables[1]); + }), + new Object[]{0, "ONE"}); + } + + @Test + public void floatTest() { + Object[] variables = new Object[]{0, (float) 0.1}; + + typeTest("ydb_float", "Float", + YdbFloatDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((float) variables[1]); + }), + variables); + + typeTest("ydb_float_class", "Float", + YdbFloatClassDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Float) variables[1]); + }), + variables); + } + + @Test + public void int8Test() { + Object[] variables = new Object[]{0, (byte) 1}; + + typeTest("ydb_byte", "Int8", + YdbByteDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((byte) variables[1]); + }), + variables); + + typeTest("ydb_byte_class", "Int8", + YdbByteClassDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Byte) variables[1]); + }), + variables); + } + + @Test + public void int16Test() { + Object[] variables = new Object[]{0, (short) 1}; + + typeTest("ydb_short", "Int16", + YdbShortDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((short) variables[1]); + }), + variables); + + typeTest("ydb_short_class", "Int16", + YdbShortClassDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Short) variables[1]); + }), + variables); + } + + @Test + public void int32Test() { + Object[] variables = new Object[]{0, 1}; + + typeTest("ydb_int", "Int32", + YdbIntDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((int) variables[1]); + }), + variables); + + typeTest("ydb_integer", "Int32", + YdbIntegerDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Integer) variables[1]); + }), + variables); + + Object[] variables2 = new Object[]{0, new Time(0)}; + + typeTest("ydb_time", "Int32", + YdbTimeDraft.$.produce(t -> { + t.setId((Integer) variables2[0]); + t.setValue((Time) variables2[1]); + }), + variables2); + } + + @Test + public void int64Test() { + Object[] variables = new Object[]{0, new BigInteger("1234567890")}; + + typeTest("ydb_big_integer", "Int64", + YdbBigIntegerDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((BigInteger) variables[1]); + }), + variables); + + Object[] variables1 = new Object[]{0, (long) 1}; + + typeTest("ydb_long", "Int64", + YdbLongDraft.$.produce(t -> { + t.setId((Integer) variables1[0]); + t.setValue((long) variables1[1]); + }), + variables1); + + typeTest("ydb_long_class", "Int64", + YdbLongClassDraft.$.produce(t -> { + t.setId((Integer) variables1[0]); + t.setValue((Long) variables1[1]); + }), + variables1); + } + + @Test + public void interval64Test() { + Object[] variables = new Object[]{0, Duration.ofHours(1)}; + + typeTest("ydb_duration", "Interval64", + YdbDurationDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Duration) variables[1]); + }), + variables); + } + + @Test + public void stringTest() { + Object[] variables = new Object[]{0, new byte[]{1, 2}}; + + typeTest("ydb_byte_array", "String", + YdbByteArrayDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((byte[]) variables[1]); + }), + variables); + } + + @Test + public void timestamp64Test() { + Object[] variables = new Object[]{0, new Timestamp(0)}; + + typeTest("ydb_timestamp", "Timestamp64", + YdbTimestampDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Timestamp) variables[1]); + }), + variables); + } + + @Test + public void utf8Test() { + Object[] variables = new Object[]{0, "ydb"}; + + typeTest("ydb_string", "Utf8", + YdbStringDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((String) variables[1]); + }), + variables); + } + + @Test + public void uuidTest() { + Object[] variables = new Object[]{0, UUID.fromString("9e197d65-1914-4d57-a65f-77a52a06baa7")}; + + typeTest("ydb_uuid", "Uuid", + YdbUuidDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((UUID) variables[1]); + }), + variables); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertJsonTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertJsonTest.java new file mode 100644 index 00000000..32e219a4 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertJsonTest.java @@ -0,0 +1,45 @@ +package ydb.jimmer.dialect.dataTypeTest; + +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.model.type.ydbJson.Json; +import ydb.jimmer.dialect.model.type.ydbJson.YdbJsonDraft; +import ydb.jimmer.dialect.model.type.ydbJson.YdbJsonTable; + +public class InsertJsonTest extends AbstractSelectTest { + private static final String TABLE_NAME = "ydb_json"; + private static final String TYPE_NAME = "Json"; + + @Test + public void insertJsonTest() { + Json json = new Json(); + json.setA(2); + json.setB(3); + + Object[] variables = new Object[]{0, json}; + + String[] expectedValues = new String[]{"{\"a\":2,\"b\":3}"}; + String expectedJson = buildJsonResponse(expectedValues); + + YdbJsonTable table = YdbJsonTable.$; + + createTable(TABLE_NAME, TYPE_NAME); + + getIsolationClient().getEntities().saveCommand(YdbJsonDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Json) variables[1]); + })) + .setMode(SaveMode.INSERT_ONLY).execute(); + executeAndExpect(getYqlClient().createQuery(table).select(table), + cxt -> { + cxt.sql("insert into " + TABLE_NAME + "(id, value) values(?, ?)"); + cxt.nextStatement(); + cxt.sql("select tb_1_.id, tb_1_.value from " + TABLE_NAME + " tb_1_"); + cxt.rows(expectedJson); + } + ); + + dropTable(TABLE_NAME); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/SelectDataTypeTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/SelectDataTypeTest.java new file mode 100644 index 00000000..63e62097 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/SelectDataTypeTest.java @@ -0,0 +1,316 @@ +package ydb.jimmer.dialect.dataTypeTest; + +import org.babyfish.jimmer.sql.ast.PropExpression; +import org.babyfish.jimmer.sql.ast.table.spi.AbstractTypedTable; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.model.type.ydbBool.YdbBooleanClassTable; +import ydb.jimmer.dialect.model.type.ydbBool.YdbBooleanTable; +import ydb.jimmer.dialect.model.type.ydbDate32.YdbDateTable; +import ydb.jimmer.dialect.model.type.ydbDate32.YdbLocalDateTable; +import ydb.jimmer.dialect.model.type.ydbDatetime64.YdbLocalDateTimeTable; +import ydb.jimmer.dialect.model.type.ydbDecimal.YdbBigDecimalTable; +import ydb.jimmer.dialect.model.type.ydbDouble.YdbDoubleClassTable; +import ydb.jimmer.dialect.model.type.ydbDouble.YdbDoubleTable; +import ydb.jimmer.dialect.model.type.ydbEnum.YdbEnumTable; +import ydb.jimmer.dialect.model.type.ydbFloat.YdbFloatClassTable; +import ydb.jimmer.dialect.model.type.ydbFloat.YdbFloatTable; +import ydb.jimmer.dialect.model.type.ydbInt16.YdbShortClassTable; +import ydb.jimmer.dialect.model.type.ydbInt16.YdbShortTable; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbIntTable; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbIntegerTable; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbLocalTimeTable; +import ydb.jimmer.dialect.model.type.ydbInt32.YdbTimeTable; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbBigIntegerTable; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbLongClassTable; +import ydb.jimmer.dialect.model.type.ydbInt64.YdbLongTable; +import ydb.jimmer.dialect.model.type.ydbInt8.YdbByteClassTable; +import ydb.jimmer.dialect.model.type.ydbInt8.YdbByteTable; +import ydb.jimmer.dialect.model.type.ydbInterval64.YdbDurationTable; +import ydb.jimmer.dialect.model.type.ydbJson.YdbJsonTable; +import ydb.jimmer.dialect.model.type.ydbString.YdbByteArrayTable; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbInstantTable; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbTimestampTable; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbUtilDateTable; +import ydb.jimmer.dialect.model.type.ydbUtf8.YdbStringTable; +import ydb.jimmer.dialect.model.type.ydbUuid.YdbUuidTable; + +import java.time.ZoneId; + +public class SelectDataTypeTest extends AbstractSelectTest { + private void typeTest(String tableName, + String typeName, + AbstractTypedTable table, + PropExpression prop, + String[] values) { + typeTest(tableName, typeName, table, prop, values, values); + } + + private void typeTest(String tableName, + String typeName, + AbstractTypedTable table, + PropExpression prop, + String[] valuesToInsert, + String[] expectedValues) { + createTable(tableName, typeName); + + insert(tableName, valuesToInsert); + + String json = buildJsonResponse(expectedValues); + + var query = getYqlClient().createQuery(table); + StringBuilder sql = new StringBuilder("select tb_1_.id, tb_1_.value from " + tableName + " tb_1_"); + if (expectedValues.length > 1) { + query = query.orderBy(prop); + sql.append(" order by tb_1_.value asc"); + } + + executeAndExpect( + query.select(table), + cxt -> { + cxt.sql(sql.toString()); + cxt.rows(json); + } + ); + + dropTable(tableName); + } + + /** + * {@link org.babyfish.jimmer.sql.ast.impl.Variables#handleDateTime(Object, ZoneId) handleDateTime(Object, ZoneId)} + * this Jimmer method is responsible for changing java types without any user input + */ + @Test + public void handleDateTimeJimmerTest() { + String[] values = new String[]{"Timestamp64(\"2017-11-27T13:24:00.123456Z\")"}; + String[] expectedValues = new String[]{"\"2017-11-27T13:24:00.123456Z\""}; + + typeTest("ydb_instant", "Timestamp64", + YdbInstantTable.$, YdbInstantTable.$.value(), + values, expectedValues); + + values = new String[]{"DateTime64(\"2017-11-27T13:24:00Z\")"}; + expectedValues = new String[]{"\"2017-11-27T13:24:00\""}; + + typeTest("ydb_local_date_time", "DateTime64", + YdbLocalDateTimeTable.$, YdbLocalDateTimeTable.$.value(), + values, expectedValues); + + values = new String[]{"Date32(\"144169-01-01\")"}; + expectedValues = new String[]{"\"+144169-01-01\""}; + + typeTest("ydb_local_date", "Date32", + YdbLocalDateTable.$, YdbLocalDateTable.$.value(), + values, expectedValues); + + values = new String[]{"-1", "0", "10"}; + expectedValues = new String[]{"\"02:59:59.999\"", "\"03:00:00\"", "\"03:00:00.01\""}; + + typeTest("ydb_local_time", "Int32", + YdbLocalTimeTable.$, YdbLocalTimeTable.$.value(), + values, expectedValues); + + values = new String[]{"Timestamp64(\"2017-11-27T13:24:00.123456Z\")"}; + expectedValues = new String[]{"\"2017-11-27\""}; + + typeTest("ydb_util_date", "Timestamp64", + YdbUtilDateTable.$, YdbUtilDateTable.$.value(), + values, expectedValues); + } + + @Test + public void boolTest() { + String[] values = new String[]{"false", "true"}; + + typeTest("ydb_boolean", "Bool", + YdbBooleanTable.$, YdbBooleanTable.$.value(), + values); + + typeTest("ydb_boolean_class", "Bool", + YdbBooleanClassTable.$, YdbBooleanClassTable.$.value(), + values); + } + + @Test + public void date32Test() { + String[] values = new String[]{"Date32(\"144169-01-01\")"}; + String[] expectedValues = new String[]{"\"4169-01-01\""}; + + typeTest("ydb_date", "Date32", + YdbDateTable.$, YdbDateTable.$.value(), + values, expectedValues); + } + + @Test + public void decimalTest() { + String[] values = new String[]{"Decimal(\"1.23\", 22, 9)"}; + String[] expectedValues = new String[]{"1.230000000"}; + + typeTest("ydb_big_decimal", "Decimal(22, 9)", + YdbBigDecimalTable.$, YdbBigDecimalTable.$.value(), + values, expectedValues); + } + + @Test + public void doubleTest() { + String[] values = new String[]{"Double(\"1.23\")"}; + String[] expectedValues = new String[]{"1.23"}; + + typeTest("ydb_double", "Double", + YdbDoubleTable.$, YdbDoubleTable.$.value(), + values, expectedValues); + + typeTest("ydb_double_class", "Double", + YdbDoubleClassTable.$, YdbDoubleClassTable.$.value(), + values, expectedValues); + } + + @Test + public void enumTest() { + String[] values = new String[]{"\"ONE\"", "\"TWO\""}; + + typeTest("ydb_enum", "Utf8", + YdbEnumTable.$, YdbEnumTable.$.value(), + values); + } + + @Test + public void floatTest() { + String[] values = new String[]{"Float(\"1.23\")"}; + String[] expectedValues = new String[]{"1.23"}; + + typeTest("ydb_float", "Float", + YdbFloatTable.$, YdbFloatTable.$.value(), + values, expectedValues); + + typeTest("ydb_float_class", "Float", + YdbFloatClassTable.$, YdbFloatClassTable.$.value(), + values, expectedValues); + } + + @Test + public void int8Test() { + String[] values = new String[]{"-1", "0", "10"}; + + typeTest("ydb_byte", "Int8", + YdbByteTable.$, YdbByteTable.$.value(), + values); + + typeTest("ydb_byte_class", "Int8", + YdbByteClassTable.$, YdbByteClassTable.$.value(), + values); + } + + @Test + public void int16Test() { + String[] values = new String[]{"-1", "0", "10"}; + + typeTest("ydb_short", "Int16", + YdbShortTable.$, YdbShortTable.$.value(), + values); + + typeTest("ydb_short_class", "Int16", + YdbShortClassTable.$, YdbShortClassTable.$.value(), + values); + } + + @Test + public void int32Test() { + String[] values = new String[]{"-1", "0", "10"}; + + typeTest("ydb_int", "Int32", + YdbIntTable.$, YdbIntTable.$.value(), + values); + + typeTest("ydb_integer", "Int32", + YdbIntegerTable.$, YdbIntegerTable.$.value(), + values); + + values = new String[]{"0", "10"}; + String[] expectedValues = new String[]{"\"00:00:00\"", "\"00:00:10\""}; + + typeTest("ydb_time", "Int32", + YdbTimeTable.$, YdbTimeTable.$.value(), + values, expectedValues); + } + + @Test + public void int64Test() { + String[] values = new String[]{"-1", "0", "10"}; + + typeTest("ydb_big_integer", "Int64", + YdbBigIntegerTable.$, YdbBigIntegerTable.$.value(), + values); + + typeTest("ydb_long", "Int64", + YdbLongTable.$, YdbLongTable.$.value(), + values); + + typeTest("ydb_long_class", "Int64", + YdbLongClassTable.$, YdbLongClassTable.$.value(), + values); + } + + @Test + public void interval64Test() { + String[] values = new String[]{"Interval(\"P0DT0H0M0.567890S\")"}; + String[] expectedValues = new String[]{"0.567890000"}; + + typeTest("ydb_duration", "Interval64", + YdbDurationTable.$, YdbDurationTable.$.value(), + values, expectedValues); + } + + @Test + public void jsonTest() { + String[] values = new String[]{"Json(@@{\"a\":1,\"b\":null}@@)"}; + String[] expectedValues = new String[]{"{\"a\":1,\"b\":null}"}; + + typeTest("ydb_json", "Json", + YdbJsonTable.$, YdbJsonTable.$.value(), + values, expectedValues); + } + + @Test + public void stringTest() { + String[] values = new String[]{"\"0\"", "\"string\""}; + String[] expectedValues = new String[]{"\"MA==\"", "\"c3RyaW5n\""}; + + typeTest("ydb_byte_array", "String", + YdbByteArrayTable.$, YdbByteArrayTable.$.value(), + values, expectedValues); + } + + @Test + public void timestamp64Test() { + String[] values = new String[]{"Timestamp64(\"2017-11-27T13:24:00.123456Z\")"}; + String[] expectedValues = new String[]{"\"2017-11-27\""}; + + typeTest("ydb_timestamp", "Timestamp64", + YdbTimestampTable.$, YdbTimestampTable.$.value(), + values, expectedValues); + } + + @Test + public void utf8Test() { + String[] values = new String[]{"\"0\"", "\"string\""}; + + typeTest("ydb_string", "Utf8", + YdbStringTable.$, YdbStringTable.$.value(), + values); + } + + @Test + public void uuidTest() { + String[] values = new String[]{ + "Uuid(\"9e197d65-1914-4d57-a65f-77a52a06baa7\")", + "Uuid(\"8e0f2cf4-4656-4d73-970e-a18be9ead78b\")"}; + String[] expectedValues = new String[]{ + "\"9e197d65-1914-4d57-a65f-77a52a06baa7\"", + "\"8e0f2cf4-4656-4d73-970e-a18be9ead78b\""}; + + typeTest("ydb_uuid", "Uuid", + YdbUuidTable.$, YdbUuidTable.$.value(), + values, expectedValues); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Entity.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Entity.java new file mode 100644 index 00000000..39a70aca --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Entity.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.model; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@org.babyfish.jimmer.sql.Entity +@Table(name = "simple_table") +public interface Entity { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + int value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/EntityUuid.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/EntityUuid.java new file mode 100644 index 00000000..05681f68 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/EntityUuid.java @@ -0,0 +1,18 @@ +package ydb.jimmer.dialect.model; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.util.UUID; + +@org.babyfish.jimmer.sql.Entity +@Table(name = "benchmark_items") +public interface EntityUuid { + @Id + @Column(name = "id") + UUID getId(); + + @Column(name = "value") + String value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Group.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Group.java new file mode 100644 index 00000000..cb69a3b0 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Group.java @@ -0,0 +1,22 @@ +package ydb.jimmer.dialect.model; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.GeneratedValue; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; +import org.babyfish.jimmer.sql.meta.UUIDIdGenerator; + +import java.util.UUID; + +@Entity +@Table(name = "group") +public interface Group { + @Id + @GeneratedValue(generatorType = UUIDIdGenerator.class) + @Column(name = "id") + UUID id(); + + @Column(name = "name") + String name(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Student.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Student.java new file mode 100644 index 00000000..3ba0119b --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/Student.java @@ -0,0 +1,31 @@ +package ydb.jimmer.dialect.model; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.ForeignKeyType; +import org.babyfish.jimmer.sql.GeneratedValue; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.JoinColumn; +import org.babyfish.jimmer.sql.ManyToOne; +import org.babyfish.jimmer.sql.Table; +import org.babyfish.jimmer.sql.meta.UUIDIdGenerator; + +import javax.annotation.Nullable; +import java.util.UUID; + +@Entity +@Table(name = "student") +public interface Student { + @Id + @GeneratedValue(generatorType = UUIDIdGenerator.class) + @Column(name = "id") + UUID id(); + + @Column(name = "name") + String name(); + + @ManyToOne + @Nullable + @JoinColumn(name = "group", foreignKeyType = ForeignKeyType.FAKE) + Group group(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/streaming/YdbStreaming.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/streaming/YdbStreaming.java new file mode 100644 index 00000000..b725e1df --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/streaming/YdbStreaming.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.streaming; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_streaming") +public interface YdbStreaming { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + int value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/transaction/YdbTransaction.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/transaction/YdbTransaction.java new file mode 100644 index 00000000..e2803f26 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/transaction/YdbTransaction.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.transaction; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_transaction") +public interface YdbTransaction { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + int value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBoolean.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBoolean.java new file mode 100644 index 00000000..3a17a26b --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBoolean.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbBool; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_boolean") +public interface YdbBoolean { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + boolean value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBooleanClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBooleanClass.java new file mode 100644 index 00000000..02d7da81 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbBool/YdbBooleanClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbBool; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_boolean_class") +public interface YdbBooleanClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Boolean value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbDate.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbDate.java new file mode 100644 index 00000000..3d7411bc --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbDate.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.model.type.ydbDate32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.sql.Date; +import java.time.LocalDate; + +@Entity +@Table(name = "ydb_date") +public interface YdbDate { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Date value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbLocalDate.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbLocalDate.java new file mode 100644 index 00000000..e46c2b37 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDate32/YdbLocalDate.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbDate32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.time.LocalDate; + +@Entity +@Table(name = "ydb_local_date") +public interface YdbLocalDate { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + LocalDate value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDatetime64/YdbLocalDateTime.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDatetime64/YdbLocalDateTime.java new file mode 100644 index 00000000..6ebbebb3 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDatetime64/YdbLocalDateTime.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbDatetime64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "ydb_local_date_time") +public interface YdbLocalDateTime { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + LocalDateTime value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDecimal/YdbBigDecimal.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDecimal/YdbBigDecimal.java new file mode 100644 index 00000000..b664a6e0 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDecimal/YdbBigDecimal.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbDecimal; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.math.BigDecimal; + +@Entity +@Table(name = "ydb_big_decimal") +public interface YdbBigDecimal { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + BigDecimal value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDouble.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDouble.java new file mode 100644 index 00000000..d0308496 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDouble.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbDouble; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_double") +public interface YdbDouble { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + double value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDoubleClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDoubleClass.java new file mode 100644 index 00000000..f4ec49d7 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbDouble/YdbDoubleClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbDouble; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_double_class") +public interface YdbDoubleClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Double value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/Value.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/Value.java new file mode 100644 index 00000000..24a524c9 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/Value.java @@ -0,0 +1,8 @@ +package ydb.jimmer.dialect.model.type.ydbEnum; + +import org.babyfish.jimmer.sql.EnumType; + +@EnumType(EnumType.Strategy.NAME) +public enum Value { + ONE, TWO +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/YdbEnum.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/YdbEnum.java new file mode 100644 index 00000000..0fe8f5f7 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbEnum/YdbEnum.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbEnum; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_enum") +public interface YdbEnum { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Value value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloat.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloat.java new file mode 100644 index 00000000..c3d453c8 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloat.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbFloat; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_float") +public interface YdbFloat { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + float value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloatClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloatClass.java new file mode 100644 index 00000000..5cd4fe4e --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbFloat/YdbFloatClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbFloat; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_float_class") +public interface YdbFloatClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Float value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShort.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShort.java new file mode 100644 index 00000000..c8ce74a9 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShort.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt16; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_short") +public interface YdbShort { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + short value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShortClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShortClass.java new file mode 100644 index 00000000..28ec44e0 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt16/YdbShortClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt16; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_short_class") +public interface YdbShortClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Short value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInt.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInt.java new file mode 100644 index 00000000..dc22ac17 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInt.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_int") +public interface YdbInt { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + int value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInteger.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInteger.java new file mode 100644 index 00000000..a158daf2 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbInteger.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_integer") +public interface YdbInteger { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Integer value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbLocalTime.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbLocalTime.java new file mode 100644 index 00000000..2f1528df --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbLocalTime.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbInt32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.time.LocalTime; + +@Entity +@Table(name = "ydb_local_time") +public interface YdbLocalTime { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + LocalTime value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbTime.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbTime.java new file mode 100644 index 00000000..45db18cb --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt32/YdbTime.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.model.type.ydbInt32; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.sql.Time; +import java.time.LocalTime; + +@Entity +@Table(name = "ydb_time") +public interface YdbTime { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Time value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbBigInteger.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbBigInteger.java new file mode 100644 index 00000000..78a5b138 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbBigInteger.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbInt64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.math.BigInteger; + +@Entity +@Table(name = "ydb_big_integer") +public interface YdbBigInteger { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + BigInteger value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLong.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLong.java new file mode 100644 index 00000000..0cb82320 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLong.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_long") +public interface YdbLong { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + long value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLongClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLongClass.java new file mode 100644 index 00000000..acf706ab --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt64/YdbLongClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_long_class") +public interface YdbLongClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Long value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByte.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByte.java new file mode 100644 index 00000000..83fb8eb3 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByte.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt8; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_byte") +public interface YdbByte { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + byte value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByteClass.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByteClass.java new file mode 100644 index 00000000..c05df232 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInt8/YdbByteClass.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbInt8; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_byte_class") +public interface YdbByteClass { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Byte value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInterval64/YdbDuration.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInterval64/YdbDuration.java new file mode 100644 index 00000000..ed4551bc --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbInterval64/YdbDuration.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbInterval64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.time.Duration; + +@Entity +@Table(name = "ydb_duration") +public interface YdbDuration { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Duration value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/Json.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/Json.java new file mode 100644 index 00000000..29c4febb --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/Json.java @@ -0,0 +1,25 @@ +package ydb.jimmer.dialect.model.type.ydbJson; + +import org.babyfish.jimmer.sql.Serialized; + +@Serialized +public class Json { + private Integer a; + private Integer b; + + public Integer getA() { + return a; + } + + public Integer getB() { + return b; + } + + public void setA(Integer a) { + this.a = a; + } + + public void setB(Integer b) { + this.b = b; + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/YdbJson.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/YdbJson.java new file mode 100644 index 00000000..ed48b625 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbJson/YdbJson.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbJson; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_json") +public interface YdbJson { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Json value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbString/YdbByteArray.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbString/YdbByteArray.java new file mode 100644 index 00000000..354bff96 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbString/YdbByteArray.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbString; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_byte_array") +public interface YdbByteArray { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + byte[] value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbInstant.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbInstant.java new file mode 100644 index 00000000..aef30ac3 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbInstant.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbTimestamp64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.time.Instant; + +@Entity +@Table(name = "ydb_instant") +public interface YdbInstant { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Instant value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbTimestamp.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbTimestamp.java new file mode 100644 index 00000000..8bbb9bfa --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbTimestamp.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.model.type.ydbTimestamp64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.sql.Timestamp; +import java.time.Instant; + +@Entity +@Table(name = "ydb_timestamp") +public interface YdbTimestamp { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Timestamp value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbUtilDate.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbUtilDate.java new file mode 100644 index 00000000..a4225848 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbTimestamp64/YdbUtilDate.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.model.type.ydbTimestamp64; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.sql.Timestamp; +import java.util.Date; + +@Entity +@Table(name = "ydb_util_date") +public interface YdbUtilDate { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + Date value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUtf8/YdbString.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUtf8/YdbString.java new file mode 100644 index 00000000..f30c172c --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUtf8/YdbString.java @@ -0,0 +1,17 @@ +package ydb.jimmer.dialect.model.type.ydbUtf8; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +@Entity +@Table(name = "ydb_string") +public interface YdbString { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + String value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUuid/YdbUuid.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUuid/YdbUuid.java new file mode 100644 index 00000000..2eab2801 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/type/ydbUuid/YdbUuid.java @@ -0,0 +1,19 @@ +package ydb.jimmer.dialect.model.type.ydbUuid; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Table; + +import java.util.UUID; + +@Entity +@Table(name = "ydb_uuid") +public interface YdbUuid { + @Id + @Column(name = "id") + int getId(); + + @Column(name = "value") + UUID value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/versioning/BookStore.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/versioning/BookStore.java new file mode 100644 index 00000000..081ceda8 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/model/versioning/BookStore.java @@ -0,0 +1,26 @@ +package ydb.jimmer.dialect.model.versioning; + +import org.babyfish.jimmer.sql.Column; +import org.babyfish.jimmer.sql.Entity; +import org.babyfish.jimmer.sql.Id; +import org.babyfish.jimmer.sql.Key; +import org.babyfish.jimmer.sql.KeyUniqueConstraint; +import org.babyfish.jimmer.sql.Table; +import org.babyfish.jimmer.sql.Version; + +@Entity +//@KeyUniqueConstraint +@Table(name = "version_table") +public interface BookStore { + @Id + @Column(name = "id") + int id(); + +// @Key +// @Column(name = "name") +// String name(); + + @Version + @Column(name = "value") + int value(); +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/pagination/KeysetTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/pagination/KeysetTest.java new file mode 100644 index 00000000..3ed274c1 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/pagination/KeysetTest.java @@ -0,0 +1,71 @@ +package ydb.jimmer.dialect.pagination; + +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.QueryTestContext; +import ydb.jimmer.dialect.YdbKeysetPaginator; +import ydb.jimmer.dialect.model.Entity; +import ydb.jimmer.dialect.model.EntityTable; + +import java.util.Arrays; +import java.util.List; + +public class KeysetTest extends AbstractSelectTest { + private static final String TABLE_NAME = "simple_table"; + private static final String TYPE_NAME = "Int32"; + + private static final int N = 100; + private static final int LIMIT = 20; + private static final String[] VALUES = new String[100]; + + static { + for (int i = 0; i < N; i++) { + VALUES[i] = String.valueOf(i); + } + } + + @Test + public void simpleTest() { + createTable(TABLE_NAME, TYPE_NAME); + insert(TABLE_NAME, VALUES); + + YdbKeysetPaginator paginator = new YdbKeysetPaginator(getIsolationClient()); + + EntityTable table = EntityTable.$; + + YdbKeysetPaginator.Page page = null; + for (int i = 0; i < N; i += LIMIT) { + List nextCursor = null; + if (page != null) { + nextCursor = page.getNextCursor(); + } + + page = paginator.fetchPage( + table, + List.of(table.id()), + nextCursor, + LIMIT, + item -> List.of(((Entity) item).getId()), + (q, t) -> { + q.orderBy(t.id().asc()); + return q.select(t); + } + ); + + QueryTestContext cxt = new QueryTestContext(executor.getLogs(), page.getRows()); + + StringBuilder expectedSql = new StringBuilder("select tb_1_.id, tb_1_.value from " + TABLE_NAME + " tb_1_"); + if (i != 0) { + expectedSql.append(" where (tb_1_.id) > (?)"); + } + expectedSql.append(" order by tb_1_.id asc limit ?"); + + cxt.sql(expectedSql.toString()); + + String json = buildJsonResponse(i, Arrays.copyOfRange(VALUES, i, i + LIMIT)); + cxt.rows(json); + } + + dropTable(TABLE_NAME); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/QueryLog.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/QueryLog.java new file mode 100644 index 00000000..50fba64a --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/QueryLog.java @@ -0,0 +1,34 @@ +package ydb.jimmer.dialect.sqlMonitor; + +import org.babyfish.jimmer.sql.runtime.ExecutionPurpose; + +import java.util.Collections; +import java.util.List; + +public class QueryLog { + private final String sql; + private final ExecutionPurpose purpose; + private final List> variablesList; + + public QueryLog(String sql, ExecutionPurpose purpose, List> variablesList) { + this.sql = sql; + this.purpose = purpose; + this.variablesList = variablesList; + } + + public static QueryLog simple(String sql, ExecutionPurpose purpose, List variables) { + return new QueryLog(sql, purpose, Collections.singletonList(variables)); + } + + public String getSql() { + return sql; + } + + public ExecutionPurpose getPurpose() { + return purpose; + } + + public List> getVariablesList() { + return variablesList; + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/YdbExecutorMonitor.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/YdbExecutorMonitor.java new file mode 100644 index 00000000..70cfa91d --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/sqlMonitor/YdbExecutorMonitor.java @@ -0,0 +1,68 @@ +package ydb.jimmer.dialect.sqlMonitor; + +import org.babyfish.jimmer.sql.runtime.AbstractExecutorProxy; +import org.babyfish.jimmer.sql.runtime.ExceptionTranslator; +import org.babyfish.jimmer.sql.runtime.Executor; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +public class YdbExecutorMonitor extends AbstractExecutorProxy { + private List queryLogs = new ArrayList<>(); + + public YdbExecutorMonitor(Executor raw) { + super(raw); + } + + public YdbExecutorMonitor(Executor raw, List queryLogs) { + super(raw); + this.queryLogs = queryLogs; + } + + public List getLogs() { + List tmp = queryLogs; + queryLogs = new ArrayList<>(); + return tmp; + } + + @Override + public R execute(@NotNull Args args) { + queryLogs.add(QueryLog.simple(args.sql, args.purpose, args.variables)); + return raw.execute(args); + } + + @Override + protected AbstractExecutorProxy recreate(Executor raw) { + return new YdbExecutorMonitor(raw, queryLogs); + } + + @Override + protected Batch createBatch(BatchContext raw) { + return new YdbBatch(raw, queryLogs); + } + + private static class YdbBatch extends AbstractExecutorProxy.Batch { + private final List queryLogs; + private final List> variablesList = new ArrayList<>(); + + protected YdbBatch(BatchContext raw, List queryLogs) { + super(raw); + this.queryLogs = queryLogs; + } + + @Override + public void add(List variables) { + raw.add(variables); + variablesList.add(variables); + } + + @Override + public int[] execute(BiFunction exceptionTranslator) { + queryLogs.add(new QueryLog(raw.sql(), raw.purpose(), variablesList)); + return raw.execute(exceptionTranslator); + } + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/streaming/StreamingTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/streaming/StreamingTest.java new file mode 100644 index 00000000..66e71533 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/streaming/StreamingTest.java @@ -0,0 +1,45 @@ +package ydb.jimmer.dialect.streaming; + +import org.babyfish.jimmer.sql.ast.PropExpression; +import org.babyfish.jimmer.sql.ast.table.spi.AbstractTypedTable; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.model.streaming.YdbStreamingTable; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +public class StreamingTest extends AbstractSelectTest { + private static final String TABLE_NAME = "ydb_streaming"; + private static final String TYPE_NAME = "Int32"; + + @Test + public void cursorTest() { + AbstractTypedTable table = YdbStreamingTable.$; + PropExpression prop = YdbStreamingTable.$.value(); + String[] values = new String[]{"11", "12", "21", "22"}; + + createTable(TABLE_NAME, TYPE_NAME); + + insert(TABLE_NAME, values); + + String json = buildJsonResponse(values); + + executeAndExpect((Connection con) -> { + List responses = new ArrayList<>(); + getYqlClient() + .createQuery(table) + .orderBy(prop) + .select(table) + .forEach(con, 2, responses::add); + return responses; + }, + cxt -> { + cxt.sql( + "select tb_1_.id, tb_1_.value from " + TABLE_NAME + " tb_1_ order by tb_1_.value asc"); + cxt.rows(json); + } + ); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/AbstractTransactionTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/AbstractTransactionTest.java new file mode 100644 index 00000000..45969413 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/AbstractTransactionTest.java @@ -0,0 +1,76 @@ +package ydb.jimmer.dialect.transaction; + +import org.babyfish.jimmer.sql.ast.mutation.MutationResult; +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.QueryTestContext; +import ydb.jimmer.dialect.model.transaction.YdbTransaction; +import ydb.jimmer.dialect.model.transaction.YdbTransactionDraft; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class AbstractTransactionTest extends AbstractSelectTest { + private static final String TABLE_NAME = "ydb_transaction"; + private static final String TYPE_NAME = "Int32"; + + protected static final YqlClient yqlClient = getIsolationClient(); + + protected void readTest(Function>, List> transaction) { + String[] values = new String[]{"-1", "0", "10"}; + + createTable(TABLE_NAME, TYPE_NAME); + + insert(TABLE_NAME, values); + + List rows = transaction.apply(() -> + yqlClient.getEntities().findAll(YdbTransaction.class) + ); + QueryTestContext cxt = new QueryTestContext(executor.getLogs(), rows); + + cxt.sql("select tb_1_.id, tb_1_.value from " + TABLE_NAME + " tb_1_"); + + String json = buildJsonResponse(values); + cxt.rows(json); + + dropTable(TABLE_NAME); + } + + protected void writeTest(Function, MutationResult> transaction, boolean readOnly) { + String errorMessage = null; + if (readOnly) { + errorMessage = "Cannot execute the DML statement"; + } + + writeTest(transaction, errorMessage); + } + + protected void writeTest(Function, MutationResult> transaction, String errorMessage) { + Object[] variables = new Object[]{0, 10}; + + createTable(TABLE_NAME, TYPE_NAME); + + MutationResult result = null; + Throwable throwable = null; + try { + result = transaction.apply(() -> + yqlClient.getEntities().saveCommand( + YdbTransactionDraft.$.produce(item -> { + item.setId(0); + item.setValue(10); + }) + ).setMode(SaveMode.INSERT_ONLY).execute() + ); + } catch (Throwable ex) { + throwable = ex; + } + QueryTestContext cxt = new QueryTestContext(executor.getLogs(), result, throwable); + + cxt.sql("insert into " + TABLE_NAME + "(id, value) values(?, ?)"); + cxt.variables(variables); + cxt.error(errorMessage); + + dropTable(TABLE_NAME); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineConsistentTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineConsistentTest.java new file mode 100644 index 00000000..69246211 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineConsistentTest.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.transaction; + + +import org.junit.jupiter.api.Test; + +public class OnlineConsistentTest extends AbstractTransactionTest { + @Test + public void readTest() { + readTest(yqlClient::onlineConsistentReadOnly); + } + + @Test + public void writeTest() { + writeTest(yqlClient::onlineConsistentReadOnly, true); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineInconsistentTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineInconsistentTest.java new file mode 100644 index 00000000..ce958c3c --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/OnlineInconsistentTest.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.transaction; + + +import org.junit.jupiter.api.Test; + +public class OnlineInconsistentTest extends AbstractTransactionTest { + @Test + public void readTest() { + readTest(yqlClient::onlineInconsistentReadOnly); + } + + @Test + public void writeTest() { + writeTest(yqlClient::onlineInconsistentReadOnly, true); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/RetryConfigTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/RetryConfigTest.java new file mode 100644 index 00000000..cb2dae76 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/RetryConfigTest.java @@ -0,0 +1,67 @@ +package ydb.jimmer.dialect.transaction; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +public class RetryConfigTest { + @Test + public void ceilingCalculationTest() { + RetryConfig.Builder cb = RetryConfig.builder(); + + int[] backoff = new int[] {1, 2, 3, 10}; + int[] cap = new int[] {63, 64, 65, 80, 81, 82, 100}; + int[] multiplier = new int[] {2, 3, 10}; + for (int j : backoff) { + for (int k : cap) { + for (int value : multiplier) { + checkCeilingValue(cb + .backoffBaseMs(j) + .capBackoffMs(k) + .backoffMultiplier(value) + .build()); + } + } + } + } + + private static void checkCeilingValue(RetryConfig config) { + Assertions.assertTrue( + config.fastBackoffBaseMs() * Math.pow(config.backoffMultiplier(), config.fastCeiling()) + >= config.fastCapBackoffMs()); + } + + @Test + public void illegalArgumentsCheck() { + RetryConfig.Builder cb = RetryConfig.builder(); + + int[] values = new int[] {0, -1, -10}; + for (int v : values) { + checkIfThrows(() -> cb + .maxAttempts(v) + .build()); + } + + values = new int[] {-1, -2, -10}; + for (int v : values) { + checkIfThrows(() -> cb + .backoffBaseMs(v) + .build()); + + checkIfThrows(() -> cb + .capBackoffMs(v) + .build()); + } + + double[] dvalues = new double[] {1, 0.5, 0, -0.5, -1, -10}; + for (double v : dvalues) { + checkIfThrows(() -> cb + .backoffMultiplier(v) + .build()); + } + } + + private static void checkIfThrows(Executable block) { + Assertions.assertThrows(IllegalArgumentException.class, block); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SerializableTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SerializableTest.java new file mode 100644 index 00000000..84748d58 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SerializableTest.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.transaction; + + +import org.junit.jupiter.api.Test; + +public class SerializableTest extends AbstractTransactionTest { + @Test + public void readTest() { + readTest(yqlClient::serializableReadWrite); + } + + @Test + public void writeTest() { + writeTest(yqlClient::serializableReadWrite, false); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SnapshotTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SnapshotTest.java new file mode 100644 index 00000000..6c228058 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/SnapshotTest.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.transaction; + + +import org.junit.jupiter.api.Test; + +public class SnapshotTest extends AbstractTransactionTest { + @Test + public void readTest() { + readTest(yqlClient::snapshotReadOnly); + } + + @Test + public void writeTest() { + writeTest(yqlClient::snapshotReadOnly, true); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/StaleTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/StaleTest.java new file mode 100644 index 00000000..b3c97a5a --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/StaleTest.java @@ -0,0 +1,16 @@ +package ydb.jimmer.dialect.transaction; + + +import org.junit.jupiter.api.Test; + +public class StaleTest extends AbstractTransactionTest { + @Test + public void readTest() { + readTest(yqlClient::staleReadOnly); + } + + @Test + public void writeTest() { + writeTest(yqlClient::staleReadOnly, true); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbDelayCalculatorTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbDelayCalculatorTest.java new file mode 100644 index 00000000..e376f2b3 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbDelayCalculatorTest.java @@ -0,0 +1,90 @@ +package ydb.jimmer.dialect.transaction; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class YdbDelayCalculatorTest { + @Test + void shouldCalculateBackoffFromFirstRetryWithoutZeroDelayFormula() { + RetryConfig config = RetryConfig.builder() + .maxAttempts(5) + .backoffMultiplier(100) + .build(); + + assertEquals(config.slowBackoffBaseMs(), YdbDelayCalculator.calculateBackoffMillis( + config.slowBackoffBaseMs(), + config.slowCapBackoffMs(), + config.slowCeiling(), + config.backoffMultiplier(), + 0 + )); + assertEquals(config.fastBackoffBaseMs(), YdbDelayCalculator.calculateBackoffMillis( + config.fastBackoffBaseMs(), + config.fastCapBackoffMs(), + config.fastCeiling(), + config.backoffMultiplier(), + 0 + )); + } + + @Test + void shouldClampToCapBackoff() { + RetryConfig config = RetryConfig.builder() + .maxAttempts(5) + .backoffBaseMs(1) + .capBackoffMs(300) + .build(); + + assertEquals(config.slowCapBackoffMs(), YdbDelayCalculator.calculateBackoffMillis( + config.slowBackoffBaseMs(), + config.slowCapBackoffMs(), + config.slowCeiling(), + config.backoffMultiplier(), + 9 + )); + assertEquals(config.fastCapBackoffMs(), YdbDelayCalculator.calculateBackoffMillis( + config.fastBackoffBaseMs(), + config.fastCapBackoffMs(), + config.fastCeiling(), + config.backoffMultiplier(), + 9 + )); + } + + @Test + void shouldUseInclusiveRangeForFullJitter() { + Set observed = new HashSet<>(); + + for (int i = 0; i < 256; i++) { + long delay = YdbDelayCalculator.fullJitterMillis(1, 1, 1, 10, 0); + assertTrue(delay >= 0 && delay <= 1); + observed.add(delay); + } + + assertTrue(observed.contains(0L)); + assertTrue(observed.contains(1L)); + } + + @Test + void shouldUseEqualJitterRange() { + Set observed = new HashSet<>(); + + for (int i = 0; i < 256; i++) { + long delay = YdbDelayCalculator.equalJitterMillis(2, 2, 1, 10, 0); + assertTrue(delay >= 1 && delay <= 2); + observed.add(delay); + } + + assertTrue(observed.contains(1L)); + assertTrue(observed.contains(2L)); + } + + @Test + void shouldKeepOddRemainderForFirstOverloadedRetry() { + assertEquals(1, YdbDelayCalculator.equalJitterMillis(1, 1, 1, 2, 0)); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManagerTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManagerTest.java new file mode 100644 index 00000000..faab0a0b --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManagerTest.java @@ -0,0 +1,20 @@ +package ydb.jimmer.dialect.transaction; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import ydb.jimmer.dialect.AbstractTest; + +import javax.sql.DataSource; + +public class YdbTxConnectionManagerTest extends AbstractTest { + private final DataSource dataSource = new DriverManagerDataSource(getJdbcURL()); + + @Test + public void testConnectionManager() { + YdbTxConnectionManager connectionManager = new YdbTxConnectionManager(dataSource); + + Integer value = 2026; + Assertions.assertEquals(value, connectionManager.execute((con) -> value)); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/versioning/InsertVersionTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/versioning/InsertVersionTest.java new file mode 100644 index 00000000..fc3d7070 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/versioning/InsertVersionTest.java @@ -0,0 +1,40 @@ +package ydb.jimmer.dialect.versioning; + +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import org.junit.jupiter.api.Test; +import ydb.jimmer.dialect.AbstractInsertTest; +import ydb.jimmer.dialect.model.versioning.BookStoreDraft; + +public class InsertVersionTest extends AbstractInsertTest { + private static final String TABLE_NAME = "version_table"; + private static final String TYPE_NAME = "Int32"; + + @Test + public void simpleTest() { + createTable(TABLE_NAME, TYPE_NAME); + + getIsolationClient().getEntities().saveCommand( + BookStoreDraft.$.produce(version -> + version.setId(0).setValue(0) + ) + ).setMode(SaveMode.INSERT_ONLY).execute(); + + executeAndExpect( + getYqlClient().getEntities().saveCommand( + BookStoreDraft.$.produce(version -> + version.setId(0).setValue(2) + ) + ).setMode(SaveMode.UPDATE_ONLY), + ctx -> { + ctx.nextStatement(); + ctx.sql("UPDATE " + TABLE_NAME + " set value = value + 1 where id = ? and value = ? returning id"); + ctx.error("Save error caused by the path: \"\": " + + "Cannot update the entity whose type is " + + "\"ydb.jimmer.dialect.model.versioning.BookStore\" " + + "and id is \"0\" because of optimistic lock error"); + } + ); + + dropTable(TABLE_NAME); + } +} diff --git a/jimmer-dialect/src/test/resources/database-drop-tables-ydb.sql b/jimmer-dialect/src/test/resources/database-drop-tables-ydb.sql new file mode 100644 index 00000000..0fc388ec --- /dev/null +++ b/jimmer-dialect/src/test/resources/database-drop-tables-ydb.sql @@ -0,0 +1,2 @@ +DROP TABLE group; +DROP TABLE student; \ No newline at end of file diff --git a/jimmer-dialect/src/test/resources/database-ydb.sql b/jimmer-dialect/src/test/resources/database-ydb.sql new file mode 100644 index 00000000..04c21071 --- /dev/null +++ b/jimmer-dialect/src/test/resources/database-ydb.sql @@ -0,0 +1,12 @@ +CREATE TABLE group ( + id Uuid, + name String, + PRIMARY KEY (id) +); + +CREATE TABLE student ( + id Uuid, + name String, + group Uuid, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/jimmer-dialect/src/test/resources/docker-compose.yml b/jimmer-dialect/src/test/resources/docker-compose.yml new file mode 100644 index 00000000..bfdb3a9a --- /dev/null +++ b/jimmer-dialect/src/test/resources/docker-compose.yml @@ -0,0 +1,21 @@ +services: + ydb: + image: ydbplatform/local-ydb:latest + container_name: jimmer_test_ydb + restart: always + platform: "linux/amd64" + ports: + - "2135:2135" + - "2136:2136" + - "8765:8765" + environment: + GRPC_TLS_PORT: 2135 + GRPC_PORT: 2136 + MON_PORT: 8765 + volumes: + - ydb_certs:/ydb_certs + - ydb_data:/ydb_data + +volumes: + ydb_certs: + ydb_data: \ No newline at end of file diff --git a/jimmer-dialect/src/test/resources/install.sh b/jimmer-dialect/src/test/resources/install.sh new file mode 100644 index 00000000..7ffa300d --- /dev/null +++ b/jimmer-dialect/src/test/resources/install.sh @@ -0,0 +1,17 @@ +docker run \ + --restart=always \ + -d \ + --name jimmer_test_ydb \ + -h localhost \ + --platform linux/amd64 \ + -p 2135:2135 \ + -p 2136:2136 \ + -p 8765:8765 \ + -p 9092:9092 \ + -v $(pwd)/ydb_certs:/ydb_certs \ + -v $(pwd)/ydb_data:/ydb_data \ + -e GRPC_TLS_PORT=2135 \ + -e GRPC_PORT=2136 \ + -e MON_PORT=8765 \ + -e YDB_KAFKA_PROXY_PORT=9092 \ + ydbplatform/local-ydb:latest diff --git a/jimmer-dialect/src/test/resources/uninstall.sh b/jimmer-dialect/src/test/resources/uninstall.sh new file mode 100644 index 00000000..18083f39 --- /dev/null +++ b/jimmer-dialect/src/test/resources/uninstall.sh @@ -0,0 +1,4 @@ +docker stop jimmer_test_ydb +docker rm jimmer_test_ydb +docker volume rm ydb_certs +docker volume rm ydb_data \ No newline at end of file