diff --git a/jimmer-dialect/README.md b/jimmer-dialect/README.md new file mode 100644 index 00000000..a55f78e2 --- /dev/null +++ b/jimmer-dialect/README.md @@ -0,0 +1,34 @@ +# 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. +- YDB keyset pagination. + +## 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..b4a893b0 --- /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 + compile + + + 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 + compile + + + + 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..b91918eb --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/UuidTransactionCacheOperator.java @@ -0,0 +1,135 @@ +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.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() + .registerModule(new JavaTimeModule()) + .registerModule(new ImmutableModule()); + 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/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/YdbTxConnectionManager.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManager.java new file mode 100644 index 00000000..fd89ae2b --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YdbTxConnectionManager.java @@ -0,0 +1,243 @@ +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.sql.SQLTimeoutException; +import java.util.function.Function; + +/** + * Provides propagation, isolation level, read only mode + * and retry from abort/timeout for transactions. + */ +public class YdbTxConnectionManager implements TxConnectionManager { + private final DataSource dataSource; + private final ThreadLocal scopeLocal = new ThreadLocal<>(); + + private final int BACKOFF_MULTIPLIER = 2; + + 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(int maxAttempts, long retryDelayMs, Function block) { + return executeTransaction(maxAttempts, retryDelayMs, Propagation.REQUIRES_NEW, block); + } + + @Override + public final R executeTransaction(Propagation propagation, Function block) { + return executeTransaction(1, 0, propagation, block); + } + + public final R executeTransaction( + int maxAttempts, + long retryDelayMs, + Propagation propagation, + Function block + ) { + for (int i = 0; i < maxAttempts; i++) { + try { + Scope parent = scopeLocal.get(); + Scope scope = createScope(parent, propagation); + scopeLocal.set(scope); + try { + R result; + try { + result = execute(scope.con, block); + } catch (RuntimeException | Error ex) { + scope.terminate(true); + throw ex; + } + scope.terminate(false); + return result; + } finally { + if (parent != null) { + scopeLocal.set(parent); + } else { + scopeLocal.remove(); + } + } + } catch (SQLException | RuntimeException ex) { + RuntimeException e = new ExecutionException("JDBC error raised: " + ex.getMessage(), ex); + if (ex instanceof RuntimeException rex) { + e = rex; + } + + if (i == maxAttempts - 1 || !isRetryable(e.getCause())) { + throw e; + } + } + + try { + Thread.sleep(retryDelayMs); + retryDelayMs *= BACKOFF_MULTIPLIER; + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + } + + throw new RuntimeException("Max attempts exceeded"); + } + + private boolean isRetryable(Throwable ex) { + return ex instanceof SQLTimeoutException || + (ex instanceof SQLException && isRetryableSqlState((SQLException) ex)); + } + + private boolean isRetryableSqlState(SQLException ex) { + String sqlState = ex.getSQLState(); + return sqlState != null && ( + sqlState.startsWith("40") || // Transaction rollback + sqlState.startsWith("08") // Connection exception + ); + } + + 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/YqlClient.java b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YqlClient.java new file mode 100644 index 00000000..1f379a16 --- /dev/null +++ b/jimmer-dialect/src/main/java/ydb/jimmer/dialect/transaction/YqlClient.java @@ -0,0 +1,66 @@ +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(int maxAttempts, long retryDelayMs, Supplier block) { + if (getConnectionManager() instanceof YdbTxConnectionManager ydbCM) { + return ydbCM.executeTransaction(maxAttempts, retryDelayMs, con -> block.get()); + } + + throw new IllegalStateException( + "The connection manager does not support retries for transactions. " + + "Use YdbTxConnectionManager." + ); + } + + public R withIsolation(int isolationLevel, boolean readOnly, Supplier block) { + try { + TransactionContext.setSettings(isolationLevel, readOnly); + return transaction(block); + } finally { + TransactionContext.clear(); + } + } + + public R serializableReadWrite(Supplier block) { + return withIsolation(Connection.TRANSACTION_SERIALIZABLE, false, block); + } + + public R snapshotReadOnly(Supplier block) { + return withIsolation(Connection.TRANSACTION_SERIALIZABLE, true, block); + } + + public R staleReadOnly(Supplier block) { + return withIsolation(YdbConst.STALE_READ_ONLY, true, block); + } + + public R onlineConsistentReadOnly(Supplier block) { + return withIsolation(YdbConst.ONLINE_CONSISTENT_READ_ONLY, true, block); + } + + public R onlineInconsistentReadOnly(Supplier block) { + return withIsolation(YdbConst.ONLINE_INCONSISTENT_READ_ONLY, true, 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..128fdc6d --- /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 variables: " + + Arrays.toString(values) + + ", actual variables: " + + 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/CrudScanBenchmarkTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/CrudScanBenchmarkTest.java new file mode 100644 index 00000000..8f5efede --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/CrudScanBenchmarkTest.java @@ -0,0 +1,183 @@ +package ydb.jimmer.dialect.benchmark; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.ast.mutation.SaveMode; +import org.babyfish.jimmer.sql.runtime.ConnectionManager; +import org.junit.jupiter.api.Test; +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.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.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.format.OutputFormat; +import org.openjdk.jmh.runner.format.OutputFormatFactory; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.VerboseMode; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.YdbDialect; +import ydb.jimmer.dialect.YqlClientBuilder; +import ydb.jimmer.dialect.model.Entity; +import ydb.jimmer.dialect.model.EntityDraft; +import ydb.jimmer.dialect.model.EntityTable; +import ydb.jimmer.dialect.transaction.YqlClient; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(0) +public class CrudScanBenchmarkTest extends AbstractSelectTest { + private static final String TABLE_NAME = "simple_table"; + private static final String TYPE_NAME = "Int32"; + + private static final int size = 1000; + + private static final String[] values; + private static final List existingEntities; + private static final List existingIds; + private static final List newEntities; + + static { + values = new String[size]; + existingEntities = new ArrayList<>(); + existingIds = new ArrayList<>(); + for (int i = 0; i < size; i++) { + int value = i; + values[i] = String.valueOf(value); + existingEntities.add(EntityDraft.$.produce(entity -> { + entity.setId(value); + entity.setValue(value); + })); + existingIds.add(value); + } + + newEntities = new ArrayList<>(); + for (int i = 0; i < size; i++) { + int value = size + i; + newEntities.add(EntityDraft.$.produce(entity -> { + entity.setId(value); + entity.setValue(value); + })); + } + } + + protected static JSqlClient sqlClient; + protected static YqlClient yqlClient; + + static { + DataSource dataSource = new DriverManagerDataSource(getJdbcURL()); + sqlClient = JSqlClient.newBuilder() + .setDialect(new YdbDialect()) + .setConnectionManager(ConnectionManager.simpleConnectionManager(dataSource)) + .build(); + yqlClient = YqlClientBuilder.getYqlClient(dataSource); + } + + @Setup(Level.Invocation) + public void setup() { + createTable(TABLE_NAME, TYPE_NAME); + + yqlClient.getEntities() + .saveEntitiesCommand(existingEntities) + .execute(); + } + + @TearDown(Level.Invocation) + public void tearDown() { + dropTable(TABLE_NAME); + } + + @Benchmark + public void benchmarkRead() { + yqlClient.snapshotReadOnly(() -> yqlClient.getEntities().findAll(Entity.class)); + } + + @Benchmark + public void benchmarkScan() { + EntityTable table = EntityTable.$; + yqlClient.createQuery(table) + .where(table.value().ge(size/2)) + .where(table.value().le(size)) + .select(table); + } + + @Benchmark + public void benchmarkScanDialectOnly() { + EntityTable table = EntityTable.$; + sqlClient.createQuery(table) + .where(table.value().ge(size/2)) + .where(table.value().le(size)) + .select(table); + } + + @Benchmark + public void benchmarkReadDialectOnly() { + sqlClient.getEntities().findAll(Entity.class); + } + + @Benchmark + public void benchmarkSave() { + yqlClient.getEntities() + .saveEntitiesCommand(newEntities) + .execute(); + } + + @Benchmark + public void benchmarkSaveDialectOnly() { + sqlClient.getEntities() + .saveEntitiesCommand(newEntities) + .execute(); + } + + @Benchmark + public void benchmarkUpdate() { + yqlClient.getEntities() + .saveEntitiesCommand(existingEntities) + .setMode(SaveMode.UPDATE_ONLY) + .execute(); + } + + @Benchmark + public void benchmarkDelete() { + yqlClient.getEntities().deleteAll(Entity.class, existingIds); + } + + @Benchmark + public void benchmarkDeleteDialectOnly() { + sqlClient.getEntities().deleteAll(Entity.class, existingIds); + } + + @Test + void runBenchmarks() throws RunnerException { + Options opts = new OptionsBuilder() + .include(CrudScanBenchmarkTest.class.getSimpleName()) + .shouldFailOnError(true) + .shouldDoGC(true) + .build(); + + OutputFormat out = OutputFormatFactory.createFormatInstance( + System.out, + VerboseMode.NORMAL + ); + + + new Runner(opts, out).run(); + } +} diff --git a/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/JoinBenchmarkTest.java b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/JoinBenchmarkTest.java new file mode 100644 index 00000000..e7780a01 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/benchmark/JoinBenchmarkTest.java @@ -0,0 +1,174 @@ +package ydb.jimmer.dialect.benchmark; + +import org.babyfish.jimmer.sql.JSqlClient; +import org.babyfish.jimmer.sql.JoinType; +import org.babyfish.jimmer.sql.runtime.ConnectionManager; +import org.junit.jupiter.api.Test; +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.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.format.OutputFormat; +import org.openjdk.jmh.runner.format.OutputFormatFactory; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.VerboseMode; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import ydb.jimmer.dialect.AbstractSelectTest; +import ydb.jimmer.dialect.YdbDialect; +import ydb.jimmer.dialect.YqlClientBuilder; +import ydb.jimmer.dialect.model.Group; +import ydb.jimmer.dialect.model.GroupDraft; +import ydb.jimmer.dialect.model.Student; +import ydb.jimmer.dialect.model.StudentDraft; +import ydb.jimmer.dialect.model.StudentTable; +import ydb.jimmer.dialect.transaction.YqlClient; + +import javax.sql.DataSource; +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.Benchmark) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(0) +public class JoinBenchmarkTest extends AbstractSelectTest { + private static final int size = 1000; + + private static final List students; + private static final List groups; + + static { + students = new ArrayList<>(); + groups = new ArrayList<>(); + for (int i = 0; i < size; i++) { + UUID value = UUID.randomUUID(); + Group group = GroupDraft.$.produce(g -> { + g.setId(value); + g.setName("name" + value); + }); + groups.add(group); + students.add(StudentDraft.$.produce(entity -> { + entity.setId(UUID.randomUUID()); + entity.setName("name1" + value); + entity.setGroup(group); + })); + students.add(StudentDraft.$.produce(entity -> { + entity.setId(UUID.randomUUID()); + entity.setName("name2" + value); + entity.setGroup(group); + })); + } + } + + protected static JSqlClient sqlClient; + protected static YqlClient yqlClient; + + static { + DataSource dataSource = new DriverManagerDataSource(getJdbcURL()); + sqlClient = JSqlClient.newBuilder() + .setDialect(new YdbDialect()) + .setConnectionManager(ConnectionManager.simpleConnectionManager(dataSource)) + .build(); + yqlClient = YqlClientBuilder.getYqlClient(dataSource); + } + + @Setup(Level.Invocation) + public void setup() { + initDatabase(); + + yqlClient.getEntities() + .saveEntitiesCommand(students) + .execute(); + + yqlClient.getEntities() + .saveEntitiesCommand(groups) + .execute(); + } + + @Benchmark + public void benchmarkLeft() { + benchmarkJoin(JoinType.LEFT); + } + + @Benchmark + public void benchmarkLeftDialectOnly() { + benchmarkJoinDialectOnly(JoinType.LEFT); + } + + @Benchmark + public void benchmarkRight() { + benchmarkJoin(JoinType.RIGHT); + } + + @Benchmark + public void benchmarkRightDialectOnly() { + benchmarkJoinDialectOnly(JoinType.RIGHT); + } + + @Benchmark + public void benchmarkInner() { + benchmarkJoin(JoinType.INNER); + } + + @Benchmark + public void benchmarkInnerDialectOnly() { + benchmarkJoinDialectOnly(JoinType.INNER); + } + + @Benchmark + public void benchmarkFull() { + benchmarkJoin(JoinType.FULL); + } + + @Benchmark + public void benchmarkFullDialectOnly() { + benchmarkJoinDialectOnly(JoinType.FULL); + } + + private void benchmarkJoin(JoinType joinType) { + StudentTable table = StudentTable.$; + yqlClient + .createQuery(table) + .orderBy(table.group(joinType).name().asc()) + .select(table); + } + + private void benchmarkJoinDialectOnly(JoinType joinType) { + StudentTable table = StudentTable.$; + sqlClient + .createQuery(table) + .orderBy(table.group(joinType).name().asc()) + .select(table); + } + + @Test + void runBenchmarks() throws RunnerException { + Options opts = new OptionsBuilder() + .include(JoinBenchmarkTest.class.getSimpleName()) + .shouldFailOnError(true) + .shouldDoGC(true) + .build(); + + OutputFormat out = OutputFormatFactory.createFormatInstance( + System.out, + VerboseMode.NORMAL + ); + + + new Runner(opts, out).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..fd54b9b2 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/AlwaysFail.java @@ -0,0 +1,13 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; + +import java.sql.SQLTimeoutException; +import java.util.function.Supplier; + +public class AlwaysFail implements Supplier { + @Override + public R get() { + throw new ExecutionException("Persistent failure", new SQLTimeoutException()); + } +} 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..cfcd274f --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/FailFirstN.java @@ -0,0 +1,36 @@ +package ydb.jimmer.dialect.chaosTests; + +import org.babyfish.jimmer.sql.exception.ExecutionException; + +import java.sql.SQLTimeoutException; +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 SQLTimeoutException()); + } + + 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..e866933a --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/chaosTests/RetryTest.java @@ -0,0 +1,70 @@ +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; + +public class RetryTest extends AbstractSelectTest { + @Test + public void succeedAfter1Failure() { + int maxAttempts = 2; + + FailFirstN chaosPolicy = new FailFirstN<>(maxAttempts - 1); + Assertions.assertDoesNotThrow(() -> + getIsolationClient().transaction( + maxAttempts, 0, + chaosPolicy + ) + ); + + Assertions.assertEquals(maxAttempts, chaosPolicy.getAttempt()); + } + + @Test + public void failAllAttempts() { + int maxAttempts = 3; + + Assertions.assertThrows( + ExecutionException.class, () -> + getIsolationClient().transaction( + maxAttempts, 0, + new AlwaysFail<>() + ) + ); + } + + @Test + public void failWithoutRetries() { + int maxAttempts = 3; + + NotRetryableFail chaosPolicy = new NotRetryableFail<>(); + Assertions.assertThrows( + ExecutionException.class, () -> + getIsolationClient().transaction( + maxAttempts, 0, + 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( + maxAttempts, 100, + chaosPolicy + ) + ); + long elapsed = System.currentTimeMillis() - start; + + Assertions.assertTrue(elapsed >= 300, "the backoff time is less than 100ms"); + } +} 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..d00cfef4 --- /dev/null +++ b/jimmer-dialect/src/test/java/ydb/jimmer/dialect/dataTypeTest/InsertDataTypeTest.java @@ -0,0 +1,368 @@ +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.ydbDate32.YdbLocalDateDraft; +import ydb.jimmer.dialect.model.type.ydbDatetime64.YdbLocalDateTimeDraft; +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.YdbLocalTimeDraft; +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.YdbInstantDraft; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbTimestampDraft; +import ydb.jimmer.dialect.model.type.ydbTimestamp64.YdbUtilDateDraft; +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.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +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); + } + + /** + * {@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() { + Object[] variables = new Object[]{0, Instant.now()}; + + typeTest("ydb_instant", "Timestamp64", + YdbInstantDraft.$.produce(t -> { + t.setId((Integer) variables[0]); + t.setValue((Instant) variables[1]); + }), + variables); + + Object[] variables1 = new Object[]{0, LocalDateTime.parse("1970-01-01T00:00:00")}; + + typeTest("ydb_local_date_time", "DateTime64", + YdbLocalDateTimeDraft.$.produce(t -> { + t.setId((Integer) variables1[0]); + t.setValue((LocalDateTime) variables1[1]); + }), + variables1); + + Object[] variables2 = new Object[]{0, LocalDate.parse("1970-01-01")}; + + typeTest("ydb_local_date", "Date32", + YdbLocalDateDraft.$.produce(t -> { + t.setId((Integer) variables2[0]); + t.setValue((LocalDate) variables2[1]); + }), + variables2); + + Object[] variables3 = new Object[]{0, LocalTime.parse("10:15")}; + + typeTest("ydb_local_time", "Int32", + YdbLocalTimeDraft.$.produce(t -> { + t.setId((Integer) variables3[0]); + t.setValue((LocalTime) variables3[1]); + }), + variables3); + + Object[] variables4 = new Object[]{0, new java.util.Date(0)}; + + typeTest("ydb_util_date", "Timestamp64", + YdbUtilDateDraft.$.produce(t -> { + t.setId((Integer) variables4[0]); + t.setValue((java.util.Date) variables4[1]); + }), + variables4); + } + + @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/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/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/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