diff --git a/databind/src/main/java/tech/ydb/yoj/InternalApi.java b/databind/src/main/java/tech/ydb/yoj/InternalApi.java index 2f5183e6..8cc97d78 100644 --- a/databind/src/main/java/tech/ydb/yoj/InternalApi.java +++ b/databind/src/main/java/tech/ydb/yoj/InternalApi.java @@ -18,7 +18,7 @@ * Annotates internal YOJ implementation details (classes, interfaces, methods, fields, constants, etc.) that need to be * {@code public} to be used from different and/or multiple packages, but are not stable even across minor YOJ releases. *

Non-{@code public} (e.g., package-private) classes, interfaces, methods, fields, constants etc. in YOJ are assumed - * to be internal implementation details regardless of the presence of an {@code @InternalApi} annotation on them. + * to be internal implementation details regardless of the presence of an {@code @InternalApi} annotation on them. */ @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE, MODULE, RECORD_COMPONENT}) @Retention(SOURCE) diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/StdReflector.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/StdReflector.java index c1c357b0..8b78c03f 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/StdReflector.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/StdReflector.java @@ -49,7 +49,7 @@ public ReflectType reflectFieldType(Type genericType, FieldValueType bindingT } private ReflectType reflectFor(Type type, FieldValueType fvt) { - Class rawType = TypeToken.of(type).getRawType(); + Class rawType = type instanceof Class clazz ? clazz : TypeToken.of(type).getRawType(); for (TypeFactory m : matchers) { if (m.matches(rawType, fvt)) { return m.create(this, rawType, fvt); diff --git a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryDataShard.java b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryDataShard.java index 7ad8de6b..f406b55a 100644 --- a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryDataShard.java +++ b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryDataShard.java @@ -2,7 +2,6 @@ import tech.ydb.yoj.databind.schema.Schema; import tech.ydb.yoj.repository.db.Entity; -import tech.ydb.yoj.repository.db.EntityIdSchema; import tech.ydb.yoj.repository.db.EntitySchema; import tech.ydb.yoj.repository.db.Range; import tech.ydb.yoj.repository.db.Table; @@ -18,18 +17,19 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; import java.util.TreeMap; /*package*/ final class InMemoryDataShard> { private final TableDescriptor tableDescriptor; private final EntitySchema schema; - private final TreeMap, InMemoryEntityLine> entityLines; + private final SortedMap, InMemoryEntityLine> entityLines; private final Map>> uncommited = new HashMap<>(); private InMemoryDataShard( TableDescriptor tableDescriptor, EntitySchema schema, - TreeMap, InMemoryEntityLine> entityLines + SortedMap, InMemoryEntityLine> entityLines ) { this.tableDescriptor = tableDescriptor; this.schema = schema; @@ -39,18 +39,24 @@ private InMemoryDataShard( public InMemoryDataShard(TableDescriptor tableDescriptor) { this( tableDescriptor, - EntitySchema.of(tableDescriptor.entityType()), - createEmptyLines(tableDescriptor.entityType()) + EntitySchema.of(tableDescriptor.entityType()) ); } - private static > TreeMap, InMemoryEntityLine> createEmptyLines(Class type) { - return new TreeMap<>(EntityIdSchema.getIdComparator(type)); + private InMemoryDataShard( + TableDescriptor tableDescriptor, + EntitySchema schema + ) { + this(tableDescriptor, schema, createEmptyLines(schema)); + } + + private static > SortedMap, InMemoryEntityLine> createEmptyLines(EntitySchema schema) { + return new TreeMap<>(schema.getIdSchema()); } public synchronized InMemoryDataShard createSnapshot() { - TreeMap, InMemoryEntityLine> snapshotLines = createEmptyLines(tableDescriptor.entityType()); - for (Map.Entry, InMemoryEntityLine> entry : entityLines.entrySet()) { + var snapshotLines = createEmptyLines(schema); + for (var entry : entityLines.entrySet()) { snapshotLines.put(entry.getKey(), entry.getValue().createSnapshot()); } return new InMemoryDataShard<>(tableDescriptor, schema, snapshotLines); diff --git a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryRepositoryTransaction.java b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryRepositoryTransaction.java index 67e2d917..a0059f31 100644 --- a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryRepositoryTransaction.java +++ b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryRepositoryTransaction.java @@ -3,6 +3,7 @@ import com.google.common.base.Stopwatch; import com.google.common.collect.Iterables; import lombok.Getter; +import tech.ydb.yoj.DeprecationWarnings; import tech.ydb.yoj.repository.BaseDb; import tech.ydb.yoj.repository.db.Entity; import tech.ydb.yoj.repository.db.RepositoryTransaction; @@ -57,7 +58,7 @@ private long getVersion() { @Override public > Table table(Class c) { - return new InMemoryTable<>(getMemory(c)); + return new InMemoryTable<>(this, c); } @Override @@ -65,8 +66,13 @@ public > Table table(TableDescriptor tableDescriptor) return new InMemoryTable<>(this, tableDescriptor); } - @Deprecated // use other constructor of InMemoryTable + /** + * @deprecated {@code DbMemory} and this method will be removed in YOJ 3.0.0. + */ + @Deprecated(forRemoval = true) public final > InMemoryTable.DbMemory getMemory(Class c) { + DeprecationWarnings.warnOnce("InMemoryTable.getMemory", + "InMemoryTable.getMemory(Class) will be removed in YOJ 3.0.0. Please stop using this method"); return new InMemoryTable.DbMemory<>(c, this); } diff --git a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryTable.java b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryTable.java index b4c2f084..e47f5014 100644 --- a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryTable.java +++ b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/InMemoryTable.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import tech.ydb.yoj.DeprecationWarnings; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderExpression; import tech.ydb.yoj.databind.schema.ObjectSchema; @@ -14,6 +15,7 @@ import tech.ydb.yoj.repository.db.Range; import tech.ydb.yoj.repository.db.Table; import tech.ydb.yoj.repository.db.TableDescriptor; +import tech.ydb.yoj.repository.db.TableQueryBuilder; import tech.ydb.yoj.repository.db.TableQueryImpl; import tech.ydb.yoj.repository.db.ViewSchema; import tech.ydb.yoj.repository.db.cache.FirstLevelCache; @@ -32,19 +34,28 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toUnmodifiableMap; import static java.util.stream.Collectors.toUnmodifiableSet; +import static tech.ydb.yoj.repository.db.TableQueryImpl.getEntityByIdComparator; public class InMemoryTable> implements Table { private final EntitySchema schema; private final TableDescriptor tableDescriptor; private final InMemoryRepositoryTransaction transaction; - @Deprecated // Don't use DbMemory, use other constructor instead + /** + * @deprecated {@code DbMemory} and this constructor will be removed in YOJ 3.0.0. + * Please use other constructors instead. + */ + @Deprecated(forRemoval = true) public InMemoryTable(DbMemory memory) { this(memory.transaction(), memory.type()); + DeprecationWarnings.warnOnce("new InMemoryTable(DbMemory)", + "Please do not use the InMemoryTable(DbMemory) constructor, it will be removed in YOJ 3.0.0"); } public InMemoryTable(InMemoryRepositoryTransaction transaction, Class type) { - this(transaction, TableDescriptor.from(EntitySchema.of(type))); + this.schema = EntitySchema.of(type); + this.tableDescriptor = TableDescriptor.from(schema); + this.transaction = transaction; } public InMemoryTable(InMemoryRepositoryTransaction transaction, TableDescriptor tableDescriptor) { @@ -53,6 +64,11 @@ public InMemoryTable(InMemoryRepositoryTransaction transaction, TableDescriptor< this.transaction = transaction; } + @Override + public TableQueryBuilder query() { + return new TableQueryBuilder<>(this, schema); + } + @Override public List findAll() { transaction.getWatcher().markTableRead(tableDescriptor); @@ -103,7 +119,7 @@ public List find( @Nullable Long offset ) { // NOTE: InMemoryTable doesn't handle index. - return InMemoryQueries.find(() -> findAll().stream(), filter, orderBy, limit, offset); + return TableQueryImpl.find(() -> findAll().stream(), schema, filter, orderBy, limit, offset); } @Override @@ -173,8 +189,8 @@ public TableDescriptor getTableDescriptor() { @Override public T find(Entity.Id id) { - if (id.isPartial()) { - throw new IllegalArgumentException("Cannot use partial id in find method"); + if (TableQueryImpl.isPartialId(id, schema)) { + throw new IllegalArgumentException("Cannot use partial ID in Table.find() method"); } return transaction.getTransactionLocal().firstLevelCache(tableDescriptor).get(id, __ -> { markKeyRead(id); @@ -185,13 +201,13 @@ public T find(Entity.Id id) { @Override public > List find(Set ids) { - return TableQueryImpl.find(this, getFirstLevelCache(), ids); + return TableQueryImpl.find(this, schema, getFirstLevelCache(), ids); } @Override public V find(Class viewType, Entity.Id id) { - if (id.isPartial()) { - throw new IllegalArgumentException("Cannot use partial id in find method"); + if (TableQueryImpl.isPartialId(id, schema)) { + throw new IllegalArgumentException("Cannot use partial ID in Table.find() method"); } FirstLevelCache cache = transaction.getTransactionLocal().firstLevelCache(tableDescriptor); @@ -208,10 +224,13 @@ public V find(Class viewType, Entity.Id id) { @Override @SuppressWarnings("unchecked") public > List find(Range range) { - transaction.getWatcher().markRangeRead(tableDescriptor, range); + Preconditions.checkArgument(range.getType() == schema.getIdSchema(), + "ID schema mismatch: Range was constructed with a different ID schema than the YdbTable"); + + markRangeRead(range); return findAll0().stream() .filter(e -> range.contains((ID) e.getId())) - .sorted(EntityIdSchema.SORT_ENTITY_BY_ID) + .sorted(getEntityByIdComparator(schema)) .collect(toList()); } @@ -232,7 +251,7 @@ public > List find(Class viewType, @Override public > List find(Class viewType, Set ids) { - return find(viewType, ids, null, EntityExpressions.defaultOrder(getType()), null); + return find(viewType, ids, null, EntityExpressions.defaultOrder(schema), null); } @Override @@ -404,9 +423,10 @@ private boolean isPrefixedFields(List keyFields, Set fields) { private > void markKeyRead(ID id) { EntityIdSchema> idSchema = schema.getIdSchema(); - if (idSchema.flattenFieldNames().size() != idSchema.flatten(id).size()) { + Map eqMap = idSchema.flatten(id); + if (idSchema.flattenFieldNames().size() != eqMap.size()) { // Partial key, will throw error when not searching by PK prefix - transaction.getWatcher().markRangeRead(tableDescriptor, Range.create(id, id)); + transaction.getWatcher().markRangeRead(tableDescriptor, Range.create(idSchema, eqMap)); } else { transaction.getWatcher().markRowRead(tableDescriptor, id); } @@ -475,7 +495,7 @@ public > Stream streamPartial(ID partial, int batchSi Preconditions.checkArgument(1 <= batchSize && batchSize <= 5000, "batchSize must be in range [1, 5000], got %s", batchSize); - Range range = partial == null ? null : Range.create(partial); + Range range = rangeForPartialId(partial); markRangeRead(range); return streamPartial0(range); @@ -508,13 +528,13 @@ public > Stream streamPartialIds(ID partial, int bat Preconditions.checkArgument(1 <= batchSize && batchSize <= 10000, "batchSize must be in range [1, 10000], got %s", batchSize); - Range range = partial == null ? null : Range.create(partial); + Range range = rangeForPartialId(partial); markRangeRead(range); return streamPartial0(range).map(e -> (ID) e.getId()); } - private > void markRangeRead(Range range) { + private > void markRangeRead(@Nullable Range range) { if (range == null) { transaction.getWatcher().markTableRead(tableDescriptor); } else { @@ -522,6 +542,12 @@ private > void markRangeRead(Range range) { } } + @Nullable + private > Range rangeForPartialId(ID partial) { + EntityIdSchema idSchema = schema.getIdSchema(); + return partial == null ? null : Range.create(idSchema, idSchema.flatten(partial)); + } + private > Stream readTableStream(ReadTableParams params) { if (!transaction.getOptions().getIsolationLevel().isReadOnly()) { throw new IllegalTransactionIsolationLevelException("readTable", transaction.getOptions().getIsolationLevel()); @@ -533,7 +559,7 @@ private > Stream readTableStream(ReadTableParams .stream() .filter(e -> readTableFilter(e, params)); if (params.isOrdered()) { - stream = stream.sorted(EntityIdSchema.SORT_ENTITY_BY_ID); + stream = stream.sorted(getEntityByIdComparator(schema)); } if (params.getRowLimit() > 0) { stream = stream.limit(params.getRowLimit()); @@ -542,18 +568,20 @@ private > Stream readTableStream(ReadTableParams } private > boolean readTableFilter(T e, ReadTableParams params) { + EntityIdSchema idSchema = schema.getIdSchema(); + @SuppressWarnings("unchecked") ID id = (ID) e.getId(); ID from = params.getFromKey(); if (from != null) { - int compare = EntityIdSchema.ofEntity(id.getType()).compare(id, from); + int compare = idSchema.compare(id, from); if (params.isFromInclusive() ? compare < 0 : compare <= 0) { return false; } } ID to = params.getToKey(); if (to != null) { - int compare = EntityIdSchema.ofEntity(id.getType()).compare(id, to); + int compare = idSchema.compare(id, to); return params.isToInclusive() ? compare <= 0 : compare < 0; } return true; @@ -587,7 +615,12 @@ private static > V toView( } - @Deprecated // Legacy. Using only for creating InMemoryTable. Use constructor of InMemoryTable instead + /** + * @deprecated Legacy class, used only for creating {@code InMemoryTable}. + * This class will be removed in YOJ 3.0.0. + * Please use other constructors of {@code InMemoryTable} instead. + */ + @Deprecated(forRemoval = true) public record DbMemory>( Class type, InMemoryRepositoryTransaction transaction diff --git a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java index 75cdcac5..6ac89c05 100644 --- a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java +++ b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java @@ -105,7 +105,7 @@ public SupabubbleTable supabubbles() { @Override public Supabubble2Table supabubbles2() { - return new Supabubble2InMemoryTable(getMemory(Supabubble2.class)); + return new Supabubble2InMemoryTable(this, Supabubble2.class); } @Override @@ -145,8 +145,8 @@ public Table multiWrappedEntities2() { } private static class Supabubble2InMemoryTable extends InMemoryTable implements TestEntityOperations.Supabubble2Table { - public Supabubble2InMemoryTable(DbMemory memory) { - super(memory); + public Supabubble2InMemoryTable(InMemoryRepositoryTransaction transaction, Class type) { + super(transaction, type); } } diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/ListingTest.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/ListingTest.java index 90c615ca..8a280878 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/ListingTest.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/ListingTest.java @@ -1,6 +1,5 @@ package tech.ydb.yoj.repository.test; -import org.assertj.core.api.Assertions; import org.junit.Test; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderExpression; @@ -10,7 +9,6 @@ import tech.ydb.yoj.repository.db.list.ListRequest; import tech.ydb.yoj.repository.db.list.ListRequest.ListingParams; import tech.ydb.yoj.repository.db.list.ListResult; -import tech.ydb.yoj.repository.db.list.ViewListResult; import tech.ydb.yoj.repository.test.entity.TestEntities; import tech.ydb.yoj.repository.test.sample.TestDb; import tech.ydb.yoj.repository.test.sample.TestDbImpl; @@ -65,28 +63,28 @@ public void basic() { OrderExpression orderBy = newOrderBuilder(Project.class).orderBy("name").descending().build(); FilterExpression filter = newFilterBuilder(Project.class).where("name").in("AAA", "XXX", "ZZZ").build(); db.tx(() -> { - ListResult page1 = listProjects(ListRequest.builder(Project.class) + ListResult page1 = db.projects().list(ListRequest.builder(Project.class) .pageSize(1) .orderBy(orderBy) .filter(filter) .build()); - Assertions.assertThat(page1).containsExactly(p3); + assertThat(page1).containsExactly(p3); - ListResult page2 = listProjects(ListRequest.builder(Project.class) + ListResult page2 = db.projects().list(ListRequest.builder(Project.class) .pageSize(1) .orderBy(orderBy) .filter(filter) .offset(1) .build()); - Assertions.assertThat(page2).containsExactly(p2); + assertThat(page2).containsExactly(p2); - ListResult page3 = listProjects(ListRequest.builder(Project.class) + ListResult page3 = db.projects().list(ListRequest.builder(Project.class) .pageSize(1) .orderBy(orderBy) .filter(filter) .offset(2) .build()); - Assertions.assertThat(page3).containsExactly(p1); + assertThat(page3).containsExactly(p1); assertThat(page3.isLastPage()).isTrue(); }); } @@ -100,11 +98,11 @@ public void complexIdRange() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(3) .filter(fb -> fb.where("id.a").eq(999_999)) .build()); - Assertions.assertThat(page).containsExactly(c3, c2, c1); + assertThat(page).containsExactly(c3, c2, c1); assertThat(page.isLastPage()).isTrue(); }); } @@ -118,11 +116,11 @@ public void complexIdFullScan() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(3) .filter(fb -> fb.where("id.c").eq("UUU")) .build()); - Assertions.assertThat(page).containsExactly(c2); + assertThat(page).containsExactly(c2); assertThat(page.isLastPage()).isTrue(); }); } @@ -131,7 +129,7 @@ public void complexIdFullScan() { public void failOnZeroPageSize() { db.tx(() -> { assertThatExceptionOfType(BadPageSize.class).isThrownBy(() -> - listProjects(ListRequest.builder(Project.class) + db.projects().list(ListRequest.builder(Project.class) .pageSize(0) .build())); }); @@ -141,7 +139,7 @@ public void failOnZeroPageSize() { public void failOnTooLargePageSize() { db.tx(() -> { assertThatExceptionOfType(BadPageSize.class).isThrownBy(() -> - listProjects(ListRequest.builder(Project.class) + db.projects().list(ListRequest.builder(Project.class) .pageSize(100_000) .build())); }); @@ -151,7 +149,7 @@ public void failOnTooLargePageSize() { public void failOnTooLargeOffset() { db.tx(() -> { assertThatExceptionOfType(BadOffset.class).isThrownBy(() -> - listProjects(ListRequest.builder(Project.class) + db.projects().list(ListRequest.builder(Project.class) .offset(10_001) .build())); }); @@ -166,10 +164,10 @@ public void defaultOrderingIsByIdAscending() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(4) .build()); - Assertions.assertThat(page).containsExactly(c4, c3, c2, c1); + assertThat(page).containsExactly(c4, c3, c2, c1); assertThat(page.isLastPage()).isTrue(); }); } @@ -184,11 +182,11 @@ public void and() { db.tx(() -> db.complexes().insert(c1, c2, c3, notInOutput)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(3) .filter(fb -> fb.where("id.a").eq(1).and("id.b").gte(100L).and("id.b").lte(300L)) .build()); - Assertions.assertThat(page).containsExactly(c1, c2, c3); + assertThat(page).containsExactly(c1, c2, c3); assertThat(page.isLastPage()).isTrue(); }); } @@ -220,7 +218,7 @@ public void embeddedNulls() { .pageSize(1) .build())); - Assertions.assertThat(lst).isEmpty(); + assertThat(lst).isEmpty(); assertThat(lst.isLastPage()).isTrue(); } @@ -263,12 +261,12 @@ public void simpleIdIn() { FilterExpression filter = newFilterBuilder(Project.class).where("id").in("uuid777", "uuid001", "uuid002").build(); OrderExpression orderBy = newOrderBuilder(Project.class).orderBy("id").ascending().build(); db.tx(() -> { - ListResult page = listProjects(ListRequest.builder(Project.class) + ListResult page = db.projects().list(ListRequest.builder(Project.class) .pageSize(100) .filter(filter) .orderBy(orderBy) .build()); - Assertions.assertThat(page).containsExactlyInAnyOrder(p1, p2, p3); + assertThat(page).containsExactlyInAnyOrder(p1, p2, p3); assertThat(page.isLastPage()).isTrue(); }); } @@ -282,17 +280,17 @@ public void complexIdIn() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(100) .filter(fb -> fb - .where("id.a").in(999_999,999_000) + .where("id.a").in(999_999, 999_000) .and("id.b").in(15L, 13L) .and("id.c").in("AAA", "CCC") .and("id.d").in(Complex.Status.OK, Complex.Status.FAIL) ) .orderBy(ob -> ob.orderBy("id").descending()) .build()); - Assertions.assertThat(page).containsExactly(c1, c3); + assertThat(page).containsExactly(c1, c3); assertThat(page.isLastPage()).isTrue(); }); } @@ -309,12 +307,12 @@ public void complexUnixTimestampRelational() { db.tx(() -> db.complexes().insert(c1, c2, c3)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(100) - .filter(fb -> fb.where("id.a").in(999_999,999_000).and("id.b").gte(now).and("id.b").lt(nowPlus2)) + .filter(fb -> fb.where("id.a").in(999_999, 999_000).and("id.b").gte(now).and("id.b").lt(nowPlus2)) .orderBy(ob -> ob.orderBy("id.a").descending()) .build()); - Assertions.assertThat(page).containsExactlyInAnyOrder(c1, c2); + assertThat(page).containsExactlyInAnyOrder(c1, c2); assertThat(page.isLastPage()).isTrue(); }); } @@ -331,12 +329,12 @@ public void complexUnixTimestampIn() { db.tx(() -> db.complexes().insert(c1, c2, c3)); db.tx(() -> { - ListResult page = listComplex(ListRequest.builder(Complex.class) + ListResult page = db.complexes().list(ListRequest.builder(Complex.class) .pageSize(100) - .filter(fb -> fb.where("id.a").in(999_999,999_000).and("id.b").in(now, nowPlus2)) + .filter(fb -> fb.where("id.a").in(999_999, 999_000).and("id.b").in(now, nowPlus2)) .orderBy(ob -> ob.orderBy("id.a").descending()) .build()); - Assertions.assertThat(page).containsExactly(c1, c3); + assertThat(page).containsExactly(c1, c3); assertThat(page.isLastPage()).isTrue(); }); } @@ -349,13 +347,13 @@ public void or() { db.tx(() -> db.projects().insert(p1, p2, notInOutput)); db.tx(() -> { - ListResult page = listProjects(ListRequest.builder(Project.class) + ListResult page = db.projects().list(ListRequest.builder(Project.class) .pageSize(100) .filter(newFilterBuilder(Project.class) .where("id").eq("uuid002").or("id").eq("uuid777") .build()) .build()); - Assertions.assertThat(page).containsExactly(p1, p2); + assertThat(page).containsExactly(p1, p2); assertThat(page.isLastPage()).isTrue(); }); } @@ -368,13 +366,13 @@ public void notOr() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - ListResult page = listProjects(ListRequest.builder(Project.class) + ListResult page = db.projects().list(ListRequest.builder(Project.class) .pageSize(100) .filter(not(newFilterBuilder(Project.class) .where("id").eq("uuid002").or("id").eq("uuid777") .build())) .build()); - Assertions.assertThat(page).containsExactly(inOutput); + assertThat(page).containsExactly(inOutput); assertThat(page.isLastPage()).isTrue(); }); } @@ -387,13 +385,13 @@ public void notRel() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - ListResult page = listProjects(ListRequest.builder(Project.class) + ListResult page = db.projects().list(ListRequest.builder(Project.class) .pageSize(100) .filter(not(newFilterBuilder(Project.class) .where("id").gt("uuid002") .build())) .build()); - Assertions.assertThat(page).containsExactly(p1); + assertThat(page).containsExactly(p1); assertThat(page.isLastPage()).isTrue(); }); } @@ -406,13 +404,13 @@ public void notIn() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - ListResult page = listProjects(ListRequest.builder(Project.class) + ListResult page = db.projects().list(ListRequest.builder(Project.class) .pageSize(100) .filter(not(newFilterBuilder(Project.class) .where("id").in("uuid002", "uuid777") .build())) .build()); - Assertions.assertThat(page).containsExactly(inOutput); + assertThat(page).containsExactly(inOutput); assertThat(page.isLastPage()).isTrue(); }); } @@ -462,13 +460,13 @@ public void listByNamesWithUnderscores() { db.tx(() -> db.typeFreaks().insert(tf)); db.tx(() -> { - ListResult page = listTypeFreak(ListRequest.builder(TypeFreak.class) + ListResult page = db.typeFreaks().list(ListRequest.builder(TypeFreak.class) .pageSize(50) .filter(newFilterBuilder(TypeFreak.class) .where("customNamedColumn").eq("CUSTOM NAMED COLUMN") .build()) .build()); - Assertions.assertThat(page).containsExactly(tf); + assertThat(page).containsExactly(tf); assertThat(page.isLastPage()).isTrue(); }); } @@ -479,7 +477,7 @@ public void listStringValuedFilteredByString() { db.tx(() -> db.typeFreaks().insert(typeFreak)); db.tx(() -> { - ListResult page = listTypeFreak(ListRequest.builder(TypeFreak.class) + ListResult page = db.typeFreaks().list(ListRequest.builder(TypeFreak.class) .pageSize(100) .filter(newFilterBuilder(TypeFreak.class) .where("ticket").eq("CLOUD-100500") @@ -496,7 +494,7 @@ public void listStringValuedFilteredByString2() { db.tx(() -> db.typeFreaks().insert(typeFreak)); db.tx(() -> { - ListResult page = listTypeFreak(ListRequest.builder(TypeFreak.class) + ListResult page = db.typeFreaks().list(ListRequest.builder(TypeFreak.class) .pageSize(100) .filter(newFilterBuilder(TypeFreak.class) .where("stringValueWrapper").eq("svw 123") @@ -514,7 +512,7 @@ public void listStringValuedFilteredByStruct() { db.tx(() -> db.typeFreaks().insert(typeFreak)); db.tx(() -> { - ListResult page = listTypeFreak(ListRequest.builder(TypeFreak.class) + ListResult page = db.typeFreaks().list(ListRequest.builder(TypeFreak.class) .pageSize(100) .filter(newFilterBuilder(TypeFreak.class) .where("ticket").eq(ticket) @@ -534,7 +532,7 @@ public void contains() { db.tx(() -> db.logEntries().insert(e1, e2, notInOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").contains("msg")) .build()); @@ -552,7 +550,7 @@ public void notContains() { db.tx(() -> db.logEntries().insert(e1, e2, inOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").doesNotContain("msg")) .build()); @@ -570,7 +568,7 @@ public void containsEscaped() { db.tx(() -> db.logEntries().insert(e1, e2, notInOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").contains("%_")) .build()); @@ -588,7 +586,7 @@ public void startsWith() { db.tx(() -> db.logEntries().insert(e1, e2, notInOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").startsWith("#tag")) .build()); @@ -606,7 +604,7 @@ public void startsWithEscaped() { db.tx(() -> db.logEntries().insert(e1, e2, notInOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").startsWith("%_")) .build()); @@ -624,7 +622,7 @@ public void endsWith() { db.tx(() -> db.logEntries().insert(e1, e2, inOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").endsWith(" #tag")) .build()); @@ -642,7 +640,7 @@ public void endsWithEscaped() { db.tx(() -> db.logEntries().insert(e1, e2, notInOutput, e3)); db.tx(() -> { - ListResult page = listLogEntries(ListRequest.builder(LogEntry.class) + ListResult page = db.logEntries().list(ListRequest.builder(LogEntry.class) .pageSize(100) .filter(fb -> fb.where("message").endsWith("%_")) .build()); @@ -650,24 +648,4 @@ public void endsWithEscaped() { assertThat(page.isLastPage()).isTrue(); }); } - - protected final ListResult listProjects(ListRequest request) { - return db.projects().list(request); - } - - protected final ListResult listComplex(ListRequest request) { - return db.complexes().list(request); - } - - protected final ListResult listTypeFreak(ListRequest request) { - return db.typeFreaks().list(request); - } - - protected final ListResult listLogEntries(ListRequest request) { - return db.logEntries().list(request); - } - - protected final ViewListResult listLogMessages(ListRequest request) { - return db.logEntries().list(LogEntry.Message.class, request); - } } diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/TableQueryBuilderTest.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/TableQueryBuilderTest.java index df8d5751..4d14937c 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/TableQueryBuilderTest.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/TableQueryBuilderTest.java @@ -1,12 +1,7 @@ package tech.ydb.yoj.repository.test; -import lombok.NonNull; -import org.assertj.core.api.Assertions; import org.junit.Test; -import tech.ydb.yoj.repository.db.Entity; import tech.ydb.yoj.repository.db.Repository; -import tech.ydb.yoj.repository.db.Table; -import tech.ydb.yoj.repository.db.TableQueryBuilder; import tech.ydb.yoj.repository.test.entity.TestEntities; import tech.ydb.yoj.repository.test.sample.TestDb; import tech.ydb.yoj.repository.test.sample.TestDbImpl; @@ -48,10 +43,6 @@ protected final Repository createRepository() { protected abstract Repository createTestRepository(); - protected > TableQueryBuilder createQueryBuilder(@NonNull Class entityClass) { - return getQueryBuilder(db.table(entityClass)); - } - @Test public void basic() { Project p1 = new Project(new Project.Id("uuid002"), "AAA"); @@ -61,36 +52,36 @@ public void basic() { db.tx(() -> db.projects().insert(p1, p2, notInOutput, p3)); db.tx(() -> { - List page1 = projectQuery() + List page1 = db.projects().query() .limit(1) .orderBy(ob -> ob.orderBy("name").descending()) .filter(fb -> fb.where("name").in("AAA", "XXX", "ZZZ")) .find(); - Assertions.assertThat(page1).containsExactly(p3); + assertThat(page1).containsExactly(p3); - List page2 = projectQuery() + List page2 = db.projects().query() .limit(1) .orderBy(ob -> ob.orderBy("name").descending()) .filter(fb -> fb.where("name").in("AAA", "XXX", "ZZZ")) .offset(1) .find(); - Assertions.assertThat(page2).containsExactly(p2); + assertThat(page2).containsExactly(p2); - List page3 = projectQuery() + List page3 = db.projects().query() .limit(1) .orderBy(ob -> ob.orderBy("name").descending()) .filter(fb -> fb.where("name").in("AAA", "XXX", "ZZZ")) .offset(2) .find(); - Assertions.assertThat(page3).containsExactly(p1); + assertThat(page3).containsExactly(p1); - List page4 = projectQuery() + List page4 = db.projects().query() .limit(1) .orderBy(ob -> ob.orderBy("name").descending()) .filter(fb -> fb.where("name").in("AAA", "XXX", "ZZZ")) .offset(3) .find(); - Assertions.assertThat(page4).isEmpty(); + assertThat(page4).isEmpty(); }); } @@ -103,11 +94,11 @@ public void complexIdRange() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(3) .filter(fb -> fb.where("id.a").eq(999_999)) .find(); - Assertions.assertThat(page).containsExactly(c3, c2, c1); + assertThat(page).containsExactly(c3, c2, c1); }); } @@ -120,11 +111,11 @@ public void complexIdFullScan() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(3) .filter(fb -> fb.where("id.c").eq("UUU")) .find(); - Assertions.assertThat(page).containsExactly(c2); + assertThat(page).containsExactly(c2); }); } @@ -137,10 +128,10 @@ public void defaultOrderingIsByIdAscending() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(4) .find(); - Assertions.assertThat(page).containsExactly(c4, c3, c2, c1); + assertThat(page).containsExactly(c4, c3, c2, c1); }); } @@ -154,17 +145,17 @@ public void and() { db.tx(() -> db.complexes().insert(c1, c2, c3, notInOutput)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(4) .filter(fb -> fb.where("id.a").eq(1).and("id.b").gte(100L).and("id.b").lte(300L)) .find(); - Assertions.assertThat(page).containsExactly(c1, c2, c3); + assertThat(page).containsExactly(c1, c2, c3); }); } @Test public void enumParsing() { - db.tx(() -> getQueryBuilder(db.typeFreaks()) + db.tx(() -> db.typeFreaks().query() .where("status").eq(Status.DRAFT) .orderBy(ob -> ob.orderBy("status").descending()) .limit(1) @@ -176,7 +167,7 @@ public void flattenedIsNull() { var tf = new TypeFreak(new TypeFreak.Id("b1p", 1), false, (byte) 0, (byte) 0, (short) 0, 0, 0, 0.0f, 0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); db.tx(() -> db.typeFreaks().insert(tf)); - List lst = db.tx(() -> getQueryBuilder(db.typeFreaks()) + List lst = db.tx(() -> db.typeFreaks().query() .where("jsonEmbedded").isNull() .limit(100) .find()); @@ -188,7 +179,7 @@ public void flattenedIsNotNull() { var tf = new TypeFreak(new TypeFreak.Id("b1p", 1), false, (byte) 0, (byte) 0, (short) 0, 0, 0, 0.0f, 0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, new TypeFreak.Embedded(new TypeFreak.A("A"), new TypeFreak.B("B")), null, null, null, null, null, null, null, null, null, null, null); db.tx(() -> db.typeFreaks().insert(tf)); - List lst = db.tx(() -> getQueryBuilder(db.typeFreaks()) + List lst = db.tx(() -> db.typeFreaks().query() .where("jsonEmbedded").isNotNull() .limit(100) .find()); @@ -199,12 +190,12 @@ public void flattenedIsNotNull() { public void filterStringValuedByString() { TypeFreak typeFreak = new TypeFreak(new TypeFreak.Id("b1p", 1), false, (byte) 0, (byte) 0, (short) 0, 0, 0, 0.0f, 0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, new TypeFreak.Ticket("CLOUD", 100500)); db.tx(() -> db.typeFreaks().insert(typeFreak)); - List lst = db.tx(() -> getQueryBuilder(db.typeFreaks()) + List lst = db.tx(() -> db.typeFreaks().query() .filter(fb -> fb.where("ticket").eq("CLOUD-100500")) .limit(1) .find()); - Assertions.assertThat(lst).containsOnly(typeFreak); + assertThat(lst).containsOnly(typeFreak); } @Test @@ -212,14 +203,14 @@ public void filterStringValuedByStruct() { TypeFreak.Ticket ticket = new TypeFreak.Ticket("CLOUD", 100500); TypeFreak typeFreak = new TypeFreak(new TypeFreak.Id("b1p", 1), false, (byte) 0, (byte) 0, (short) 0, 0, 0, 0.0f, 0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, ticket); db.tx(() -> db.typeFreaks().insert(typeFreak)); - List lst = db.tx(() -> getQueryBuilder(db.typeFreaks()) + List lst = db.tx(() -> db.typeFreaks().query() .filter(newFilterBuilder(TypeFreak.class) .where("ticket").eq(ticket) .build()) .limit(1) .find()); - Assertions.assertThat(lst).containsOnly(typeFreak); + assertThat(lst).containsOnly(typeFreak); } @Test @@ -227,12 +218,12 @@ public void embeddedNulls() { db.tx(() -> db.typeFreaks().insert( new TypeFreak(new TypeFreak.Id("b1p", 1), false, (byte) 0, (byte) 0, (short) 0, 0, 0, 0.0f, 0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) )); - List lst = db.tx(() -> getQueryBuilder(db.typeFreaks()) + List lst = db.tx(() -> db.typeFreaks().query() .filter(fb -> fb.where("embedded.a.a").eq("myfqdn")) .limit(1) .find()); - Assertions.assertThat(lst).isEmpty(); + assertThat(lst).isEmpty(); } @Test @@ -244,12 +235,12 @@ public void simpleIdIn() { db.tx(() -> db.projects().insert(p1, p2, notInOutput, p3)); db.tx(() -> { - List page = projectQuery() + List page = db.projects().query() .limit(100) .filter(fb -> fb.where("id").in("uuid777", "uuid001", "uuid002")) .orderBy(ob -> ob.orderBy("id").ascending()) .find(); - Assertions.assertThat(page).containsExactlyInAnyOrder(p1, p2, p3); + assertThat(page).containsExactlyInAnyOrder(p1, p2, p3); }); } @@ -262,7 +253,7 @@ public void complexIdIn() { db.tx(() -> db.complexes().insert(c1, c2, c3, c4)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(100) .filter(fb -> fb .where("id.a").in(999_999,999_000) @@ -272,7 +263,7 @@ public void complexIdIn() { ) .orderBy(ob -> ob.orderBy("id").descending()) .find(); - Assertions.assertThat(page).containsExactly(c1, c3); + assertThat(page).containsExactly(c1, c3); }); } @@ -288,12 +279,12 @@ public void complexUnixTimestampRelational() { db.tx(() -> db.complexes().insert(c1, c2, c3)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(100) .filter(fb -> fb.where("id.a").in(999_999,999_000).and("id.b").gte(now).and("id.b").lt(nowPlus2)) .orderBy(ob -> ob.orderBy("id.a").descending()) .find(); - Assertions.assertThat(page).containsExactlyInAnyOrder(c1, c2); + assertThat(page).containsExactlyInAnyOrder(c1, c2); }); } @@ -309,12 +300,12 @@ public void complexUnixTimestampIn() { db.tx(() -> db.complexes().insert(c1, c2, c3)); db.tx(() -> { - List page = complexQuery() + List page = db.complexes().query() .limit(100) .filter(fb -> fb.where("id.a").in(999_999, 999_000).and("id.b").in(now, nowPlus2)) .orderBy(ob -> ob.orderBy("id.a").descending()) .find(); - Assertions.assertThat(page).containsExactly(c1, c3); + assertThat(page).containsExactly(c1, c3); }); } @@ -326,12 +317,12 @@ public void or() { db.tx(() -> db.projects().insert(p1, p2, notInOutput)); db.tx(() -> { - List page = projectQuery() + List page = db.projects().query() .where("id").eq("uuid002") .or("id").eq("uuid777") .limit(100) .find(); - Assertions.assertThat(page).containsExactly(p1, p2); + assertThat(page).containsExactly(p1, p2); }); } @@ -343,13 +334,13 @@ public void notOr() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - List page = projectQuery() + List page = db.projects().query() .limit(100) .filter(not(newFilterBuilder(Project.class) .where("id").eq("uuid002").or("id").eq("uuid777") .build())) .find(); - Assertions.assertThat(page).containsExactly(inOutput); + assertThat(page).containsExactly(inOutput); }); } @@ -361,13 +352,13 @@ public void notRel() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - List page = projectQuery() + List page = db.projects().query() .limit(100) .filter(not(newFilterBuilder(Project.class) .where("id").gt("uuid002") .build())) .find(); - Assertions.assertThat(page).containsExactly(p1); + assertThat(page).containsExactly(p1); }); } @@ -379,13 +370,13 @@ public void notIn() { db.tx(() -> db.projects().insert(p1, p2, inOutput)); db.tx(() -> { - List page = projectQuery() + List page = db.projects().query() .limit(100) .filter(not(newFilterBuilder(Project.class) .where("id").in("uuid002", "uuid777") .build())) .find(); - Assertions.assertThat(page).containsExactly(inOutput); + assertThat(page).containsExactly(inOutput); }); } @@ -434,11 +425,11 @@ public void listByNamesWithUnderscores() { db.tx(() -> db.typeFreaks().insert(tf)); db.tx(() -> { - List page = typeFreakQuery() + List page = db.typeFreaks().query() .limit(50) .where("customNamedColumn").eq("CUSTOM NAMED COLUMN") .find(); - Assertions.assertThat(page).containsExactly(tf); + assertThat(page).containsExactly(tf); assertThat(page.size() < 50).isTrue(); }); } @@ -512,20 +503,4 @@ public void whereAndEquivalenceWithOr2() { .find() )).isEmpty(); } - - protected > TableQueryBuilder getQueryBuilder(@NonNull Table table) { - return new TableQueryBuilder<>(table); - } - - protected final TableQueryBuilder projectQuery() { - return createQueryBuilder(Project.class); - } - - protected final TableQueryBuilder complexQuery() { - return createQueryBuilder(Complex.class); - } - - protected final TableQueryBuilder typeFreakQuery() { - return createQueryBuilder(TypeFreak.class); - } } diff --git a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/statement/FindRangeStatement.java b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/statement/FindRangeStatement.java index d85775c4..c91a5f29 100644 --- a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/statement/FindRangeStatement.java +++ b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/statement/FindRangeStatement.java @@ -1,5 +1,6 @@ package tech.ydb.yoj.repository.ydb.statement; +import com.google.common.base.Preconditions; import lombok.AllArgsConstructor; import lombok.Getter; import tech.ydb.proto.ValueProtos; @@ -31,6 +32,9 @@ public FindRangeStatement( Range range ) { super(tableDescriptor, schema, outSchema); + Preconditions.checkArgument(range.getType() == schema.getIdSchema(), + "ID schema mismatch: Range was constructed with a different ID schema than the YdbTable"); + this.params = Stream.of(RangeBound.values()) .flatMap(b -> toParams(b.map(range).keySet(), b)) .collect(toList()); diff --git a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/BatchFindSpliterator.java b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/BatchFindSpliterator.java index 1e2e8287..f4896a72 100644 --- a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/BatchFindSpliterator.java +++ b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/BatchFindSpliterator.java @@ -3,7 +3,6 @@ import tech.ydb.yoj.databind.schema.Schema; import tech.ydb.yoj.repository.db.Entity; import tech.ydb.yoj.repository.db.EntityIdSchema; -import tech.ydb.yoj.repository.db.Range; import tech.ydb.yoj.repository.ydb.yql.YqlLimit; import tech.ydb.yoj.repository.ydb.yql.YqlOrderBy; import tech.ydb.yoj.repository.ydb.yql.YqlPredicate; @@ -32,13 +31,9 @@ abstract class BatchFindSpliterator, ID extends Entity.Id protected abstract List find(YqlStatementPart part, YqlStatementPart... otherParts); - BatchFindSpliterator(Class entityType, int batchSize) { - this(entityType, null, batchSize); - } - - BatchFindSpliterator(Class entityType, ID partial, int batchSize) { + BatchFindSpliterator(EntityIdSchema idSchema, ID partial, int batchSize) { this.batchSize = batchSize; - this.idSchema = EntityIdSchema.ofEntity(entityType); + this.idSchema = idSchema; this.orderById = YqlOrderBy.orderBy(this.idSchema .flattenFields().stream() .map(s -> new YqlOrderBy.SortKey(s.getPath(), YqlOrderBy.SortOrder.ASC)) @@ -46,8 +41,8 @@ abstract class BatchFindSpliterator, ID extends Entity.Id ); this.top = YqlLimit.top(batchSize); if (partial != null) { - Range range = Range.create(partial); - Map eqMap = range.getEqMap(); + // NB: Schema.flatten() does *not* write null-valued columns to the map + Map eqMap = idSchema.flatten(partial); this.initialPartialPredicates = this.idSchema .flattenFields().stream() .filter(f -> eqMap.containsKey(f.getName())) @@ -85,8 +80,8 @@ public boolean tryAdvance(Consumer action) { private List next() { if (lastPartialId.size() != 0 && lastPartialId.size() <= initialPartialPredicates.size()) { - // we need this if because YDB has bug for queries like - // SELECT * FROM table WHERE id = 'id' and id > 'id' + // We need this because certain versions of YDB have a bug for queries like + // SELECT * FROM table WHERE id = 'id' AND id > 'id' return List.of(); } diff --git a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/YdbTable.java b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/YdbTable.java index e99fe392..328ce6a2 100644 --- a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/YdbTable.java +++ b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/table/YdbTable.java @@ -4,6 +4,7 @@ import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import lombok.NonNull; +import tech.ydb.yoj.DeprecationWarnings; import tech.ydb.yoj.InternalApi; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderExpression; @@ -14,6 +15,7 @@ import tech.ydb.yoj.repository.db.Range; import tech.ydb.yoj.repository.db.Table; import tech.ydb.yoj.repository.db.TableDescriptor; +import tech.ydb.yoj.repository.db.TableQueryBuilder; import tech.ydb.yoj.repository.db.TableQueryImpl; import tech.ydb.yoj.repository.db.Tx; import tech.ydb.yoj.repository.db.ViewSchema; @@ -78,7 +80,16 @@ public YdbTable(Class type, QueryExecutor executor) { this.tableDescriptor = TableDescriptor.from(schema); } + /** + * @deprecated This {@code YdbTable} constructor uses reflection tricks to determine entity type, + * and will be removed in YOJ 2.7.0. + * Please migrate to {@link YdbTable#YdbTable(Class, QueryExecutor)} or + * {@link YdbTable#YdbTable(TableDescriptor, QueryExecutor)}. + */ + @Deprecated(forRemoval = true) protected YdbTable(QueryExecutor executor) { + DeprecationWarnings.warnOnce("new YdbTable(QueryExecutor)", + "Single-arg new YdbTable(QueryExecutor) will be removed in YOJ 2.7.0. Please use the two-arg constructors instead"); this.type = resolveEntityType(); this.executor = new CheckingQueryExecutor(executor); this.schema = EntitySchema.of(type); @@ -122,6 +133,11 @@ private static List toList(E first, E... rest) { return concat(Stream.of(first), stream(rest)).collect(Collectors.toList()); } + @Override + public TableQueryBuilder query() { + return new TableQueryBuilder<>(this, schema); + } + @Override public List findAll() { var statement = new FindAllYqlStatement<>(tableDescriptor, schema, schema); @@ -179,7 +195,8 @@ private Stream streamPartial( BiFunction, YqlStatementPart[], List> findMethod ) { Preconditions.checkArgument(1 <= batchSize && batchSize <= 5000, "batchSize must be in range [1, 5000], got %s", batchSize); - return StreamSupport.stream(new BatchFindSpliterator<>(type, partial, batchSize) { + EntityIdSchema> idSchema = schema.getIdSchema(); + return StreamSupport.stream(new BatchFindSpliterator<>(idSchema, partial, batchSize) { @Override protected Entity.Id getId(R r) { return idMapper.apply(r); @@ -200,7 +217,8 @@ public > Stream streamAllIds(int batchSize) { @Override public > Stream streamPartialIds(ID partial, int batchSize) { Preconditions.checkArgument(1 <= batchSize && batchSize <= 10000, "batchSize must be in range [1, 10000], got %s", batchSize); - return StreamSupport.stream(new BatchFindSpliterator<>(type, partial, batchSize) { + EntityIdSchema idSchema = schema.getIdSchema(); + return StreamSupport.stream(new BatchFindSpliterator<>(idSchema, partial, batchSize) { @Override protected ID getId(ID id) { return id; @@ -262,8 +280,8 @@ private Stream readTableStream(ReadTableMapper mapper, ReadTable @Override public T find(Entity.Id id) { - if (id.isPartial()) { - throw new IllegalArgumentException("Cannot use partial id in find method"); + if (TableQueryImpl.isPartialId(id, schema)) { + throw new IllegalArgumentException("Cannot use partial ID in Table.find() method"); } return executor.getTransactionLocal().firstLevelCache(tableDescriptor).get(id, __ -> { var statement = new FindYqlStatement<>(tableDescriptor, schema, schema); @@ -274,7 +292,7 @@ public T find(Entity.Id id) { @Override public > List find(Set ids) { - return TableQueryImpl.find(this, getFirstLevelCache(), ids); + return TableQueryImpl.find(this, schema, getFirstLevelCache(), ids); } @Override @@ -300,7 +318,7 @@ public > List find(Class viewType, @Override public > List find(Class viewType, Set ids) { - return find(viewType, ids, null, defaultOrder(type), null); + return find(viewType, ids, null, defaultOrder(schema), null); } public final List find(YqlStatementPart part, YqlStatementPart... otherParts) { @@ -363,7 +381,7 @@ public > List find(Set ids, @Nullable FilterExpression found = postLoad(findUncached(ids, filter, orderBy, limit)); if (!isPartialIdMode && ids.size() > found.size()) { Set> foundIds = found.stream().map(Entity::getId).collect(toSet()); @@ -464,14 +482,14 @@ public > List findIds(YqlStatementPart part, YqlS } private > List findIds(Collection> parts) { - EntityIdSchema idSchema = EntityIdSchema.ofEntity(type); + EntityIdSchema idSchema = schema.getIdSchema(); var statement = FindStatement.from(tableDescriptor, schema, idSchema, parts, false); return executor.execute(statement, parts); } @Override public > List findIds(Range range) { - EntityIdSchema idSchema = EntityIdSchema.ofEntity(type); + EntityIdSchema idSchema = schema.getIdSchema(); var statement = new FindRangeStatement<>(tableDescriptor, schema, idSchema, range); return executor.execute(statement, range); } @@ -481,8 +499,8 @@ public > List findIds(Set partialIds) { if (partialIds.isEmpty()) { return List.of(); } - OrderExpression order = defaultOrder(type); - EntityIdSchema idSchema = EntityIdSchema.ofEntity(type); + OrderExpression order = defaultOrder(schema); + EntityIdSchema idSchema = schema.getIdSchema(); var statement = FindInStatement.from(tableDescriptor, schema, idSchema, partialIds, null, order, null); return executor.execute(statement, partialIds); } diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java index 6b902d43..f72416b1 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java @@ -170,7 +170,7 @@ public Table multiWrappedEntities2() { private static class YdbSupabubble2Table extends YdbTable implements TestEntityOperations.Supabubble2Table { protected YdbSupabubble2Table(QueryExecutor executor) { - super(executor); + super(Supabubble2.class, executor); } @Override @@ -181,7 +181,7 @@ public List findLessThan(Supabubble2.Id id) { private static class ComplexTableImpl extends YdbTable implements ComplexTable { protected ComplexTableImpl(QueryExecutor executor) { - super(executor); + super(Complex.class, executor); } @Override @@ -192,7 +192,7 @@ public TableQueryBuilder query() { private static class BubbleTableImpl extends YdbTable implements BubbleTable { protected BubbleTableImpl(QueryExecutor executor) { - super(executor); + super(Bubble.class, executor); } @Override @@ -203,7 +203,7 @@ public void updateSomeFields(Set ids, String fieldA, String fieldB) { private static class IndexedTableImpl extends YdbTable implements IndexedTable { protected IndexedTableImpl(QueryExecutor executor) { - super(executor); + super(IndexedEntity.class, executor); } @Override diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/list/YdbListingIntegrationTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/list/YdbListingIntegrationTest.java index fa9ef3e3..636f452b 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/list/YdbListingIntegrationTest.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/list/YdbListingIntegrationTest.java @@ -41,7 +41,7 @@ public void idPredicatesAreProperlyOrdered() { assertThat(predicate.toYql(EntitySchema.of(Complex.class))).isEqualTo("(`id_a` = ?) AND (`id_b` = ?)"); db.tx(() -> { - ListResult page = listComplex(listRequest); + ListResult page = db.complexes().list(listRequest); assertThat(page).containsExactly(c3, c2, c1); assertThat(page.isLastPage()).isTrue(); }); diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/statement/FindInStatementTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/statement/FindInStatementTest.java index 89cc6499..d1a55d99 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/statement/FindInStatementTest.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/statement/FindInStatementTest.java @@ -45,7 +45,7 @@ public class FindInStatementTest { private static final Set NOT_INDEXED_KEYS = Set.of(FooIndexKey.of(1L, null, null)); private static final Set INCONSISTENT_TYPE_KEYS = Set.of(FooIndexKeyInconsistentType.of(100500L)); - private static final OrderExpression DEFAULT_ORDER = defaultOrder(Foo.class); + private static final OrderExpression DEFAULT_ORDER = defaultOrder(ENTITY_SCHEMA); private static final String INDEX_NAME = "index_by_value"; private static final String NOT_EXISTENT_INDEX_NAME = "not_existent_index"; diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java index 2d8cdc8e..9b791664 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java @@ -3,6 +3,7 @@ import com.google.common.reflect.TypeToken; import lombok.AccessLevel; import lombok.Getter; +import tech.ydb.yoj.DeprecationWarnings; import tech.ydb.yoj.ExperimentalApi; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderExpression; @@ -23,10 +24,24 @@ protected AbstractDelegatingTable(Table target) { this.target = target; } + /** + * @deprecated This constructor uses reflection tricks to guess entity type for the table, + * and will be removed in YOJ 3.0.0. + * Please use {@link AbstractDelegatingTable#AbstractDelegatingTable(Class)} or + * {@link AbstractDelegatingTable#AbstractDelegatingTable(TableDescriptor)} instead. + */ + @Deprecated(forRemoval = true) protected AbstractDelegatingTable() { + DeprecationWarnings.warnOnce("new AbstractDelegatingTable()", + "Nullary AbstractDelegatingTable constructor will be removed in YOJ 3.0.0. " + + "Please use 1-arg Class or TableDescriptor constructor instead"); this.target = BaseDb.current(BaseDb.class).table(resolveEntityType()); } + protected AbstractDelegatingTable(Class entityType) { + this.target = BaseDb.current(BaseDb.class).table(entityType); + } + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/32") protected AbstractDelegatingTable(TableDescriptor tableDescriptor) { this.target = BaseDb.current(BaseDb.class).table(tableDescriptor); @@ -38,6 +53,11 @@ private Class resolveEntityType() { }).getRawType(); } + @Override + public TableQueryBuilder query() { + return target.query(); + } + @Override public List find(@Nullable String indexName, @Nullable FilterExpression filter, @Nullable OrderExpression orderBy, @Nullable Integer limit, @Nullable Long offset) { return target.find(indexName, filter, orderBy, limit, offset); diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/Entity.java b/repository/src/main/java/tech/ydb/yoj/repository/db/Entity.java index 6ea2cf7b..78465d1f 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/Entity.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/Entity.java @@ -22,6 +22,17 @@ default E preSave() { return (E) this; } + /** + * @deprecated Please do not use Projections in new code, use YDB secondary indexes where possible! + *

If secondary indexes lack the required functionality (e.g., you need a dynamically computed index), + * use a custom {@code AbstractDelegatingTable} subclass for your entity's table and override the {@code save()}, + * {@code insert()} and {@code delete()} methods to effect changes to related entity or entities. + *

In the future, we will rework the projection functionality and will probably move it to a separate YOJ module + * (with a convenient superclass for your tables). + * + * @see GitHub Issue + */ + @Deprecated default List> createProjections() { return List.of(); } @@ -55,17 +66,32 @@ default E resolve( return Tx.Current.get().getRepositoryTransaction().table(getType()).find(this, throwIfAbsent); } + /** + * @deprecated This method will be removed in YOJ 3.0.0. Please use other ways to get entity type. + */ @SuppressWarnings("unchecked") + @Deprecated(forRemoval = true) default Class getType() { + DeprecationWarnings.warnOnce( + "Entity.Id.getType()", + "You are using Entity.Id.getType() which will be removed in YOJ 3.0.0. Please use other ways to get entity type" + ); return (Class) new TypeToken(getClass()) { }.getRawType(); } + /** + * @deprecated This method will be removed in YOJ 3.0.0. Please use other ways to check if ID is partial + * (i.e., has some of its trailing components set to {@code null} to implicitly indicate an ID range.) + */ + @Deprecated(forRemoval = true) default boolean isPartial() { - var schema = EntitySchema.of(getType()).getIdSchema(); - var columns = schema.flattenFields(); - var nonNullFields = schema.flatten(this); - return columns.size() > nonNullFields.size(); + DeprecationWarnings.warnOnce( + "Entity.Id.isPartial()", + "You are using Entity.Id.isPartial() which will be removed in YOJ 3.0.0. " + + "Please use other ways to check if ID is partial (i.e., has a suffix of nulls)" + ); + return TableQueryImpl.isPartialId(this, EntitySchema.of(getType())); } } } diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityExpressions.java b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityExpressions.java index bb14fee6..eed8db62 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityExpressions.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityExpressions.java @@ -5,14 +5,18 @@ import tech.ydb.yoj.databind.expression.FilterBuilder; import tech.ydb.yoj.databind.expression.OrderBuilder; import tech.ydb.yoj.databind.expression.OrderExpression; +import tech.ydb.yoj.databind.expression.OrderExpression.SortOrder; import tech.ydb.yoj.databind.schema.Schema; import static tech.ydb.yoj.databind.expression.OrderExpression.SortOrder.ASCENDING; +import static tech.ydb.yoj.repository.db.EntityIdSchema.ID_FIELD_NAME; public final class EntityExpressions { private EntityExpressions() { } + // USES SchemaRegistry.getDefault() + public static > FilterBuilder newFilterBuilder(@NonNull Class entityType) { return FilterBuilder.forSchema(schema(entityType)); } @@ -29,17 +33,43 @@ public static > OrderExpression unordered(@NonNull Class< return OrderExpression.unordered(schema(entityType)); } + public static > OrderExpression defaultOrder(@NonNull Class entityType) { + return orderById(entityType, ASCENDING); + } + + public static > OrderExpression orderById(Class entityType, SortOrder sortOrder) { + return orderById(schema(entityType), sortOrder); + } + private static > EntitySchema schema(@NonNull Class entityType) { return EntitySchema.of(entityType); } - public static > OrderExpression defaultOrder(@NonNull Class entityType) { - return orderById(entityType, ASCENDING); + // USES SCHEMA DIRECTLY + + public static > FilterBuilder newFilterBuilder(@NonNull Schema schema) { + return FilterBuilder.forSchema(schema); + } + + public static > OrderBuilder newOrderBuilder(@NonNull Schema schema) { + return OrderBuilder.forSchema(schema); + } + + /** + * @see OrderExpression#unordered(Schema) + */ + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115") + public static > OrderExpression unordered(@NonNull Schema schema) { + return OrderExpression.unordered(schema); + } + + public static > OrderExpression defaultOrder(@NonNull Schema schema) { + return orderById(schema, ASCENDING); } - public static > OrderExpression orderById(Class entityType, OrderExpression.SortOrder sortOrder) { - return newOrderBuilder(entityType) - .orderBy(new OrderExpression.SortKey(schema(entityType).getField(EntityIdSchema.ID_FIELD_NAME), sortOrder)) + public static > OrderExpression orderById(Schema schema, SortOrder sortOrder) { + return OrderBuilder.forSchema(schema) + .orderBy(new OrderExpression.SortKey(schema.getField(ID_FIELD_NAME), sortOrder)) .build(); } } diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java index 161e0437..fc2ffce3 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; import lombok.NonNull; +import tech.ydb.yoj.InternalApi; import tech.ydb.yoj.databind.CustomValueTypes; import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Schema; @@ -42,10 +43,23 @@ public final class EntityIdSchema> extends Schema im private static final String ID_SUBFIELD_PATH_PREFIX = ID_FIELD_NAME + PATH_DELIMITER; private static final String ID_SUBFIELD_NAME_PREFIX = ID_FIELD_NAME + NAME_DELIMITER; + /** + * @deprecated This is an internal implementation detail that's no longer used by YOJ implementation; it will be + * deleted in YOJ 2.7.0. + */ + @InternalApi + @Deprecated(forRemoval = true) public static final Comparator> SORT_ENTITY_BY_ID = Comparator.comparing( Entity::getId, (a, b) -> EntityIdSchema.ofEntity(a.getType()).compare(a, b) ); + /** + * @deprecated This is an internal implementation detail that's no longer used by YOJ implementation; it will be + * deleted in YOJ 2.7.0. ({@code EntityIdSchema} is already a {@link Comparator} for IDs, there is no need + * for this convoluted method.) + */ + @InternalApi + @Deprecated(forRemoval = true) public static > Comparator> getIdComparator(Class type) { return Comparator.comparing( id -> id, (a, b) -> EntityIdSchema.ofEntity(type).compare(a, b) @@ -115,9 +129,6 @@ public static , ID extends Entity.Id> EntityIdSchema return of(idType, null); } - /** - * @param namingStrategy naming strategy with mandatory equals/hashCode. - */ public static , ID extends Entity.Id> EntityIdSchema of( Class idType, NamingStrategy namingStrategy) { return of(SchemaRegistry.getDefault(), idType, namingStrategy); @@ -128,13 +139,12 @@ public static , ID extends Entity.Id> EntityIdSchema return of(registry, idType, null); } - /** - * @param namingStrategy naming strategy with mandatory equals/hashCode. - */ public static , ID extends Entity.Id> EntityIdSchema of( SchemaRegistry registry, Class idType, NamingStrategy namingStrategy) { - @SuppressWarnings("unchecked") Class entityType = (Class) resolveEntityType(idType); + @SuppressWarnings("unchecked") + Class entityType = (Class) TypeToken.of(idType).resolveType(ENTITY_TYPE_PARAMETER).getRawType(); + EntitySchema entitySchema = EntitySchema.of(registry, entityType, namingStrategy); return from(entitySchema); } @@ -144,10 +154,6 @@ public static , ID extends Entity.Id> EntityIdSchema return entitySchema.getRegistry().getOrCreate(EntityIdSchema.class, (k, r) -> new EntityIdSchema<>(entitySchema), key); } - static Class resolveEntityType(Class idType) { - return TypeToken.of(idType).resolveType(ENTITY_TYPE_PARAMETER).getRawType(); - } - public static boolean isIdField(@NonNull JavaField field) { return isIdFieldPath(field.getPath()); } diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/EntitySchema.java b/repository/src/main/java/tech/ydb/yoj/repository/db/EntitySchema.java index 86b0d51e..3b944795 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/EntitySchema.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/EntitySchema.java @@ -15,9 +15,11 @@ import static java.lang.String.format; import static lombok.AccessLevel.PACKAGE; +import static tech.ydb.yoj.repository.db.EntityIdSchema.ID_FIELD_NAME; public final class EntitySchema> extends Schema { private static final Type ENTITY_TYPE_PARAMETER = Entity.class.getTypeParameters()[0]; + private static final Type ENTITY_ID_TYPE_PARAMETER = Entity.Id.class.getTypeParameters()[0]; @Getter(PACKAGE) private final SchemaRegistry registry; @@ -46,51 +48,49 @@ public static > EntitySchema of(SchemaRegistry registry, } private EntitySchema(SchemaKey key, Reflector reflector, SchemaRegistry registry) { - super(checkEntityType(key), reflector); + super(key, reflector); + checkEntityType(); + this.registry = registry; } - private static > SchemaKey checkEntityType(SchemaKey key) { - Class entityType = key.clazz(); + private void checkEntityType() { + Class entityType = reflectType.getRawType(); if (!Entity.class.isAssignableFrom(entityType)) { throw new IllegalArgumentException(format( "Entity type [%s] must implement [%s]", entityType.getName(), Entity.class)); } - Class entityTypeFromEntityIface = resolveEntityTypeFromEntityIface(entityType); + TypeToken entityGenericType = TypeToken.of(entityType); + Class entityTypeFromEntityIface = entityGenericType.resolveType(ENTITY_TYPE_PARAMETER).getRawType(); if (!entityTypeFromEntityIface.equals(entityType)) { throw new IllegalArgumentException(format( "Entity type [%s] must implement [%s] specified by the same type, but it is specified by [%s]", entityType.getName(), Entity.class, entityTypeFromEntityIface.getName())); } - Class idFieldType; - try { - idFieldType = entityType.getDeclaredField(EntityIdSchema.ID_FIELD_NAME).getType(); - } catch (NoSuchFieldException e) { - throw new IllegalArgumentException(format( - "Entity type [%s] does not contain a mandatory \"%s\" field", - entityType.getName(), EntityIdSchema.ID_FIELD_NAME)); - } + ReflectField idField = reflectType.getFields().stream() + .filter(field -> ID_FIELD_NAME.equals(field.getName())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(format( + "Entity type [%s] does not contain a mandatory \"%s\" field", + entityType.getName(), ID_FIELD_NAME)) + ); + Class idFieldType = idField.getType(); if (!Entity.Id.class.isAssignableFrom(idFieldType)) { throw new IllegalArgumentException(format( "Entity ID type [%s] must implement [%s]", idFieldType.getName(), Entity.Id.class)); } - Class entityTypeFromIdType = EntityIdSchema.resolveEntityType(idFieldType); + TypeToken idFieldGenericType = TypeToken.of(idFieldType); + Class entityTypeFromIdType = idFieldGenericType.resolveType(ENTITY_ID_TYPE_PARAMETER).getRawType(); if (!entityTypeFromIdType.equals(entityType)) { throw new IllegalArgumentException(format( "An identifier field \"%s\" has a type [%s] that is not an identifier type for an entity of type [%s]", - EntityIdSchema.ID_FIELD_NAME, idFieldType.getName(), entityType.getName())); + ID_FIELD_NAME, idFieldType.getName(), entityType.getName())); } - - return key; - } - - static Class resolveEntityTypeFromEntityIface(Class entityType) { - return TypeToken.of(entityType).resolveType(ENTITY_TYPE_PARAMETER).getRawType(); } @Override diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java b/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java index ae23b01d..212f23f3 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java @@ -284,9 +284,7 @@ default long count(FilterExpression filter) { return count(null, filter); } - default TableQueryBuilder query() { - return new TableQueryBuilder<>(this); - } + TableQueryBuilder query(); /** * @deprecated Blindly setting entity fields is not recommended. Use {@code Table.modifyIfPresent()} instead, unless you diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java index 3118a78e..1b2279b3 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java @@ -3,7 +3,9 @@ import com.google.common.base.Preconditions; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import tech.ydb.yoj.DeprecationWarnings; import tech.ydb.yoj.ExperimentalApi; +import tech.ydb.yoj.InternalApi; import tech.ydb.yoj.databind.expression.FilterBuilder; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderBuilder; @@ -20,6 +22,7 @@ public final class TableQueryBuilder> { private final Table table; + private final EntitySchema schema; private Set> ids; private Set keys; @@ -33,8 +36,34 @@ public final class TableQueryBuilder> { private OrderExpression orderBy = null; + /** + * @deprecated Please use {@link TableQueryBuilder#TableQueryBuilder(Table, EntitySchema)} constructor instead. + */ + @InternalApi + @Deprecated(forRemoval = true) public TableQueryBuilder(@NonNull Table table) { + this(table, EntitySchema.of(table.getType())); + DeprecationWarnings.warnOnce("new TableQueryBuilder(Table)", + "Please use the 2-arg TableQueryBuilder constructor if you're implementing a table for new type of DB; " + + "and delegate to Table.query() in all other scenarios"); + } + + /** + * Creates a query DSL for the specified table and entity schema. + *

This constructor is an implementation detail, and should only be used when implementing + * a foundational type of table for a new type of database (e.g., what {@code YdbTable} is to YDB, and + * {@code InMemoryTable} is to in-memory YOJ DB implementation for tests). + *

In all other situations, you should obtain a {@code TableQueryBuilder} by calling {@link Table#query()}, and + * use whatever the {@code Table} returns. This includes implementing wrappers for {@code Table} (both subclasses + * of {@code AbstractDelegatingTable} and raw implementations of the {@code Table} interface). + * + * @param table table which will run the queries + * @param schema entity schema to use for the queries + */ + @InternalApi + public TableQueryBuilder(@NonNull Table table, @NonNull EntitySchema schema) { this.table = table; + this.schema = schema; } public long count() { @@ -151,7 +180,7 @@ private FilterBuilder filterBuilder() { if (filterBuilder == null) { Preconditions.checkState(filter == null, "You can't use both .where/.and/.or and .filter methods"); - filterBuilder = EntityExpressions.newFilterBuilder(table.getType()); + filterBuilder = EntityExpressions.newFilterBuilder(schema); } return filterBuilder; } @@ -205,7 +234,7 @@ private FilterExpression getFinalFilter() { @NonNull @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115") public TableQueryBuilder unordered() { - return orderBy(EntityExpressions.unordered(table.getType())); + return orderBy(EntityExpressions.unordered(schema)); } @NonNull @@ -216,11 +245,11 @@ public TableQueryBuilder orderBy(@Nullable OrderExpression orderBy) { @NonNull public TableQueryBuilder orderBy(@NonNull UnaryOperator> orderBuilderOp) { - return orderBy(orderBuilderOp.apply(EntityExpressions.newOrderBuilder(table.getType())).build()); + return orderBy(orderBuilderOp.apply(EntityExpressions.newOrderBuilder(schema)).build()); } public TableQueryBuilder defaultOrder() { - orderBy = EntityExpressions.defaultOrder(table.getType()); + orderBy = EntityExpressions.defaultOrder(schema); return this; } @@ -258,7 +287,7 @@ public TableQueryBuilder index(String indexName) { } private FilterExpression buildFilterExpression(UnaryOperator> filterBuilderOp) { - return filterBuilderOp.apply(EntityExpressions.newFilterBuilder(table.getType())).build(); + return filterBuilderOp.apply(EntityExpressions.newFilterBuilder(schema)).build(); } @RequiredArgsConstructor(access = PRIVATE) diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryImpl.java b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryImpl.java index 4f3ee950..2de49d40 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryImpl.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryImpl.java @@ -1,10 +1,16 @@ package tech.ydb.yoj.repository.db; import com.google.common.collect.Sets; +import lombok.NonNull; import tech.ydb.yoj.InternalApi; +import tech.ydb.yoj.databind.expression.FilterExpression; +import tech.ydb.yoj.databind.expression.OrderExpression; import tech.ydb.yoj.repository.db.cache.FirstLevelCache; import tech.ydb.yoj.repository.db.list.ListRequest; +import tech.ydb.yoj.util.function.StreamSupplier; +import javax.annotation.Nullable; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -12,8 +18,12 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import static tech.ydb.yoj.repository.db.list.InMemoryQueries.toComparator; +import static tech.ydb.yoj.repository.db.list.InMemoryQueries.toPredicate; /** * Utility class for {@link Table} implementation; for internal use only. @@ -26,14 +36,14 @@ private TableQueryImpl() { } public static , ID extends Entity.Id> List find( - Table table, FirstLevelCache cache, Set ids + Table table, EntitySchema schema, FirstLevelCache cache, Set ids ) { if (ids.isEmpty()) { return List.of(); } - var orderBy = EntityExpressions.defaultOrder(table.getType()); - var isPartialIdMode = ids.iterator().next().isPartial(); + var orderBy = EntityExpressions.defaultOrder(schema); + var isPartialIdMode = isPartialId(ids.iterator().next(), schema); var foundInCache = ids.stream() .filter(cache::containsKey) @@ -71,7 +81,44 @@ public static , ID extends Entity.Id> List find( Sets.difference(Sets.difference(ids, foundInDbIds), foundInCacheIds).forEach(cache::putEmpty); } - return merged.values().stream().sorted(EntityIdSchema.SORT_ENTITY_BY_ID).collect(Collectors.toList()); + return merged.values().stream() + .sorted(getEntityByIdComparator(schema)) + .collect(Collectors.toList()); + } + + public static > List find(@NonNull StreamSupplier streamSupplier, + @NonNull EntitySchema entitySchema, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit, + @Nullable Long offset) { + if (limit == null && offset != null && offset > 0) { + throw new IllegalArgumentException("offset > 0 with limit=null is not supported"); + } + + try (Stream stream = streamSupplier.stream()) { + Stream foundStream = stream; + if (filter != null) { + foundStream = foundStream.filter(toPredicate(filter)); + } + if (orderBy != null) { + foundStream = foundStream.sorted(toComparator(orderBy)); + } else { + foundStream = foundStream.sorted(getEntityByIdComparator(entitySchema)); + } + + foundStream = foundStream.skip(offset == null ? 0L : offset); + + if (limit != null) { + foundStream = foundStream.limit(limit); + } + + return foundStream.collect(toList()); + } + } + + public static > Comparator getEntityByIdComparator(EntitySchema schema) { + return Comparator.comparing(Entity::getId, schema.getIdSchema()); } public static > TableQueryBuilder toQueryBuilder(Table table, ListRequest request) { @@ -82,4 +129,11 @@ public static > TableQueryBuilder toQueryBuilder(Table .offset(request.getOffset()) .limit(request.getPageSize() + 1); } + + public static , ID extends Entity.Id> boolean isPartialId(ID id, EntitySchema schema) { + var idSchema = schema.getIdSchema(); + var columns = idSchema.flattenFields(); + var nonNullFields = idSchema.flatten(id); + return columns.size() > nonNullFields.size(); + } } diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/list/InMemoryQueries.java b/repository/src/main/java/tech/ydb/yoj/repository/db/list/InMemoryQueries.java index b6bcccc2..d7e07e99 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/list/InMemoryQueries.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/list/InMemoryQueries.java @@ -1,6 +1,7 @@ package tech.ydb.yoj.repository.db.list; import lombok.NonNull; +import tech.ydb.yoj.InternalApi; import tech.ydb.yoj.databind.expression.AndExpr; import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.ListExpr; @@ -13,6 +14,8 @@ import tech.ydb.yoj.databind.schema.Schema; import tech.ydb.yoj.databind.schema.Schema.JavaField; import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.EntitySchema; +import tech.ydb.yoj.repository.db.TableQueryImpl; import tech.ydb.yoj.util.function.StreamSupplier; import javax.annotation.Nullable; @@ -36,10 +39,17 @@ public final class InMemoryQueries { public static > ListResult list(@NonNull StreamSupplier streamSupplier, @NonNull ListRequest request) { + var schema = request.getSchema(); + if (!(schema instanceof EntitySchema entitySchema)) { + throw new IllegalArgumentException("Expected EntitySchema but got schema of type " + + schema.getClass().getName()); + } + return ListResult.forPage( request, - find( + TableQueryImpl.find( streamSupplier, + entitySchema, request.getFilter(), request.getOrderBy(), request.getPageSize() + 1, @@ -48,6 +58,14 @@ public static > ListResult list(@NonNull StreamSupplierThis method will be removed in YOJ 2.7.0. + */ + @InternalApi + @Deprecated(forRemoval = true) public static > List find(@NonNull StreamSupplier streamSupplier, @Nullable FilterExpression filter, @Nullable OrderExpression orderBy, diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/projection/RwProjectionCache.java b/repository/src/main/java/tech/ydb/yoj/repository/db/projection/RwProjectionCache.java index 76d4e856..8be70cb0 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/projection/RwProjectionCache.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/projection/RwProjectionCache.java @@ -5,6 +5,7 @@ import tech.ydb.yoj.InternalApi; import tech.ydb.yoj.repository.db.Entity; import tech.ydb.yoj.repository.db.RepositoryTransaction; +import tech.ydb.yoj.repository.db.Table; import java.util.LinkedHashMap; import java.util.Map; @@ -52,21 +53,28 @@ public void applyProjectionChanges(RepositoryTransaction transaction) { oldProjections.values().stream() .filter(e -> !newProjections.containsKey(e.getId())) - .forEach(e -> deleteEntity(transaction, e.getId())); + .forEach(e -> deleteEntity(transaction, e)); newProjections.values().stream() .filter(e -> !e.equals(oldProjections.get(e.getId()))) .forEach(e -> saveEntity(transaction, e)); } - private > void deleteEntity(RepositoryTransaction transaction, Entity.Id entityId) { - transaction.table(entityId.getType()).delete(entityId); + private > void deleteEntity(RepositoryTransaction transaction, Entity entity) { + table(transaction, entity).delete(entity.getId()); } private > void saveEntity(RepositoryTransaction transaction, Entity entity) { @SuppressWarnings("unchecked") T castedEntity = (T) entity; - transaction.table(entity.getId().getType()).save(castedEntity); + table(transaction, entity).save(castedEntity); + } + + private > Table table(RepositoryTransaction transaction, Entity entity) { + @SuppressWarnings("unchecked") + Class entityType = (Class) entity.getClass(); + + return transaction.table(entityType); } private Entity mergeOldProjections(Entity p1, Entity p2) { diff --git a/repository/src/test/java/tech/ydb/yoj/repository/db/PojoEntityTest.java b/repository/src/test/java/tech/ydb/yoj/repository/db/PojoEntityTest.java index 3704b7ca..a38ba5fb 100644 --- a/repository/src/test/java/tech/ydb/yoj/repository/db/PojoEntityTest.java +++ b/repository/src/test/java/tech/ydb/yoj/repository/db/PojoEntityTest.java @@ -8,7 +8,6 @@ import static org.junit.Assert.assertTrue; public class PojoEntityTest { - @Value private static class Ent implements Entity { @NonNull @@ -24,10 +23,11 @@ private static class Id implements Entity.Id { @Test public void testPartialId() { + var schema = EntitySchema.of(Ent.class); var completeId = new Ent.Id("a", "b"); var partialId = new Ent.Id("a", null); - assertTrue(partialId.isPartial()); - assertFalse(completeId.isPartial()); + assertTrue(TableQueryImpl.isPartialId(partialId, schema)); + assertFalse(TableQueryImpl.isPartialId(completeId, schema)); } } diff --git a/repository/src/test/java/tech/ydb/yoj/repository/db/RecordEntityTest.java b/repository/src/test/java/tech/ydb/yoj/repository/db/RecordEntityTest.java index 36a08417..237e3f1b 100644 --- a/repository/src/test/java/tech/ydb/yoj/repository/db/RecordEntityTest.java +++ b/repository/src/test/java/tech/ydb/yoj/repository/db/RecordEntityTest.java @@ -14,10 +14,11 @@ private record Id(String part1, String parts) implements Entity.Id