Skip to content

Commit ac43e11

Browse files
ArtDuclaude
andcommitted
feat(mapping): add TupleMapper utility and CRUD metadata support for key-based mapping
- Add TupleMapper utility class for easy tuple-to-POJO mapping using field format - Update CRUD operations (select, insert, get) to propagate format metadata from CrudResponse - Add getFormats() overload with CRUD metadata fallback - Add integration tests for select, insert, get with format metadata - Add test for fields filtering with GetOptions - Update TarantoolBoxClientTest to use TupleMapper instead of manual IntStream mapping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ed7c898 commit ac43e11

File tree

7 files changed

+194
-25
lines changed

7 files changed

+194
-25
lines changed

tarantool-client/src/main/java/io/tarantool/client/TarantoolClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
import io.tarantool.balancer.TarantoolBalancer;
1919
import io.tarantool.client.crud.TarantoolCrudClient;
20+
import io.tarantool.mapping.Field;
2021
import io.tarantool.mapping.TarantoolResponse;
22+
import io.tarantool.mapping.Tuple;
23+
import io.tarantool.mapping.TupleMapper;
2124
import io.tarantool.pool.IProtoClientPool;
2225

2326
/**

tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.function.Consumer;
2121
import java.util.function.Function;
2222
import java.util.stream.Collectors;
23-
import java.util.stream.IntStream;
2423
import java.util.stream.Stream;
2524

2625
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -51,6 +50,7 @@
5150
import static io.tarantool.client.box.TarantoolBoxSpace.WITHOUT_ENABLED_FETCH_SCHEMA_OPTION_FOR_TARANTOOL_LESS_3_0_0;
5251
import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper;
5352
import io.tarantool.client.BaseOptions;
53+
import io.tarantool.mapping.TupleMapper;
5454
import io.tarantool.client.ClientType;
5555
import io.tarantool.client.Options;
5656
import io.tarantool.client.TarantoolSpace;
@@ -399,12 +399,7 @@ public void testMappingWithFormat() {
399399

400400
for (Tuple<List<?>> t : list.get()) {
401401
List<io.tarantool.mapping.Field> tupleFormat = formatGetter.apply(t);
402-
List<?> dataList = t.get();
403-
Map<String, ?> map =
404-
IntStream.range(0, dataList.size())
405-
.boxed()
406-
.collect(Collectors.toMap((i) -> tupleFormat.get(i).getName(), dataList::get));
407-
result.add(objectMapper.convertValue(map, PersonWithDifferentFieldsOrder.class));
402+
result.add(TupleMapper.mapToPojo(t.get(), tupleFormat, PersonWithDifferentFieldsOrder.class));
408403
}
409404
return result;
410405
};

tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.junit.jupiter.api.Assertions.assertEquals;
2828
import static org.junit.jupiter.api.Assertions.assertFalse;
2929
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
30+
import static org.junit.jupiter.api.Assertions.assertNotNull;
3031
import static org.junit.jupiter.api.Assertions.assertNull;
3132
import static org.junit.jupiter.api.Assertions.assertThrows;
3233
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -79,6 +80,7 @@
7980
import io.tarantool.core.protocol.IProtoResponse;
8081
import io.tarantool.mapping.TarantoolResponse;
8182
import io.tarantool.mapping.Tuple;
83+
import io.tarantool.mapping.TupleMapper;
8284
import io.tarantool.mapping.crud.CrudBatchResponse;
8385
import io.tarantool.mapping.crud.CrudError;
8486
import io.tarantool.mapping.crud.CrudException;
@@ -3780,4 +3782,132 @@ private static <T> List<T> mapBatchToType(
37803782
.sorted(tupleComparator)
37813783
.collect(Collectors.toList());
37823784
}
3785+
3786+
@Test
3787+
public void testSelectWithFormatFromCrudMetadata() {
3788+
// Insert test data
3789+
Person testPerson = new Person(9999, true, "CrudFormatTest");
3790+
TarantoolCrudSpace personSpace = client.space("person");
3791+
personSpace.insert(testPerson).join();
3792+
3793+
List<Tuple<List<?>>> tuples =
3794+
personSpace.select(Collections.singletonList(
3795+
Condition.create(EQ, "id", 9999)
3796+
)).join();
3797+
3798+
assertFalse(tuples.isEmpty(), "Should return at least one tuple");
3799+
3800+
Tuple<List<?>> tuple = tuples.get(0);
3801+
assertNotNull(tuple, "Tuple should not be null");
3802+
3803+
// Check that format is available from CrudResponse metadata
3804+
List<io.tarantool.mapping.Field> format = tuple.getFormat();
3805+
assertNotNull(format, "Format should be available from CrudResponse metadata");
3806+
assertFalse(format.isEmpty(), "Format should not be empty");
3807+
3808+
// Verify format contains expected fields
3809+
List<String> fieldNames = format.stream()
3810+
.map(io.tarantool.mapping.Field::getName)
3811+
.collect(Collectors.toList());
3812+
3813+
assertTrue(fieldNames.contains("id"), "Format should contain 'id' field");
3814+
assertTrue(fieldNames.contains("name"), "Format should contain 'name' field");
3815+
assertTrue(fieldNames.contains("is_married"), "Format should contain 'is_married' field");
3816+
3817+
// Use TupleMapper to convert to POJO
3818+
PersonWithDifferentFieldsOrder mappedPerson =
3819+
TupleMapper.mapToPojo(tuple, PersonWithDifferentFieldsOrder.class);
3820+
3821+
assertNotNull(mappedPerson, "Mapped person should not be null");
3822+
assertEquals(9999, mappedPerson.getId(), "ID should match");
3823+
assertEquals("CrudFormatTest", mappedPerson.getName(), "Name should match");
3824+
assertEquals(true, mappedPerson.getIsMarried(), "isMarried should match");
3825+
}
3826+
3827+
@Test
3828+
public void testInsertAndGetWithFormatFromCrudMetadata() {
3829+
TarantoolCrudSpace personSpace = client.space("person");
3830+
int testId = 9998;
3831+
3832+
// Insert test data using List (raw tuple) to get Tuple<List<?>>
3833+
List<?> testPersonList = Arrays.asList(testId, true, "InsertGetFormatTest");
3834+
@SuppressWarnings("unchecked")
3835+
Tuple<List<?>> insertedTuple = personSpace.insert(testPersonList).join();
3836+
3837+
assertNotNull(insertedTuple, "Inserted tuple should not be null");
3838+
3839+
// Check that format is available from insert response
3840+
List<io.tarantool.mapping.Field> insertFormat = insertedTuple.getFormat();
3841+
assertNotNull(insertFormat, "Format should be available from insert response");
3842+
assertFalse(insertFormat.isEmpty(), "Format should not be empty");
3843+
3844+
// Use TupleMapper to convert inserted tuple to POJO
3845+
PersonWithDifferentFieldsOrder mappedInsertedPerson =
3846+
TupleMapper.mapToPojo(insertedTuple, PersonWithDifferentFieldsOrder.class);
3847+
3848+
assertNotNull(mappedInsertedPerson, "Mapped inserted person should not be null");
3849+
assertEquals(testId, mappedInsertedPerson.getId(), "ID should match after insert");
3850+
assertEquals("InsertGetFormatTest", mappedInsertedPerson.getName(), "Name should match after insert");
3851+
assertEquals(true, mappedInsertedPerson.getIsMarried(), "isMarried should match after insert");
3852+
3853+
// Get the record and check format
3854+
@SuppressWarnings("unchecked")
3855+
Tuple<List<?>> gotTuple = personSpace.get(Collections.singletonList(testId)).join();
3856+
3857+
assertNotNull(gotTuple, "Got tuple should not be null");
3858+
3859+
// Check that format is available from get response
3860+
List<io.tarantool.mapping.Field> getFormat = gotTuple.getFormat();
3861+
assertNotNull(getFormat, "Format should be available from get response");
3862+
assertFalse(getFormat.isEmpty(), "Format should not be empty");
3863+
3864+
// Use TupleMapper to convert got tuple to POJO
3865+
PersonWithDifferentFieldsOrder mappedGotPerson =
3866+
TupleMapper.mapToPojo(gotTuple, PersonWithDifferentFieldsOrder.class);
3867+
3868+
assertNotNull(mappedGotPerson, "Mapped got person should not be null");
3869+
assertEquals(testId, mappedGotPerson.getId(), "ID should match after get");
3870+
assertEquals("InsertGetFormatTest", mappedGotPerson.getName(), "Name should match after get");
3871+
assertEquals(true, mappedGotPerson.getIsMarried(), "isMarried should match after get");
3872+
}
3873+
3874+
@Test
3875+
public void testGetWithFieldsFilterAndFormat() {
3876+
TarantoolCrudSpace personSpace = client.space("person");
3877+
int testId = 9997;
3878+
3879+
// Insert test data first
3880+
List<?> testPersonList = Arrays.asList(testId, true, "FieldsFilterTest");
3881+
personSpace.insert(testPersonList).join();
3882+
3883+
// Get with fields filter - only request 'id' and 'name' fields
3884+
@SuppressWarnings("unchecked")
3885+
Tuple<List<?>> filteredTuple = personSpace.get(
3886+
Collections.singletonList(testId),
3887+
GetOptions.builder().withFields(Arrays.asList("id", "name")).build()
3888+
).join();
3889+
3890+
assertNotNull(filteredTuple, "Filtered tuple should not be null");
3891+
3892+
// Check that format is available and contains only requested fields
3893+
List<io.tarantool.mapping.Field> format = filteredTuple.getFormat();
3894+
assertNotNull(format, "Format should be available from filtered get response");
3895+
3896+
// The format should reflect the requested fields
3897+
List<String> fieldNames = format.stream()
3898+
.map(io.tarantool.mapping.Field::getName)
3899+
.collect(Collectors.toList());
3900+
3901+
assertTrue(fieldNames.contains("id"), "Format should contain 'id' field");
3902+
assertTrue(fieldNames.contains("name"), "Format should contain 'name' field");
3903+
3904+
// Map to POJO using TupleMapper
3905+
PersonWithDifferentFieldsOrder mappedPerson =
3906+
TupleMapper.mapToPojo(filteredTuple, PersonWithDifferentFieldsOrder.class);
3907+
3908+
assertNotNull(mappedPerson, "Mapped person should not be null");
3909+
assertEquals(testId, mappedPerson.getId(), "ID should match");
3910+
assertEquals("FieldsFilterTest", mappedPerson.getName(), "Name should match");
3911+
// isMarried might be null since we filtered the fields
3912+
}
37833913
}

tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetClass.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ public static <T> CompletableFuture<Tuple<T>> convertCrudSingleResultFuture(
6565

6666
public static <T> TarantoolResponse<List<Tuple<T>>> readCrudSingleResultData(
6767
IProtoResponse response, Class<T> entity) {
68+
CrudResponse<List<Tuple<T>>> crudResponse =
69+
readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))));
6870
return new TarantoolResponse<>(
69-
getRows(
70-
readData(
71-
response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))),
72-
getFormats(response));
71+
getRows(crudResponse),
72+
getFormats(response, crudResponse.getMetadata()));
7373
}
7474

7575
public static <T> CompletableFuture<SelectResponse<List<Tuple<T>>>> convertSelectResultFuture(
@@ -109,11 +109,11 @@ public static <T> CompletableFuture<List<Tuple<T>>> convertCrudSelectResultFutur
109109

110110
public static <T> TarantoolResponse<List<Tuple<T>>> readCrudSelectResult(
111111
IProtoResponse response, Class<T> entity) {
112+
CrudResponse<List<Tuple<T>>> crudResponse =
113+
readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))));
112114
return new TarantoolResponse<>(
113-
getRows(
114-
readData(
115-
response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))),
116-
getFormats(response));
115+
getRows(crudResponse),
116+
getFormats(response, crudResponse.getMetadata()));
117117
}
118118

119119
public static <T>

tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetTypeReference.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ public static <T> CompletableFuture<Tuple<T>> convertCrudSingleResultFuture(
5757

5858
public static <T> TarantoolResponse<List<Tuple<T>>> readCrudSingleResultData(
5959
IProtoResponse response, TypeReference<T> entity) {
60+
CrudResponse<List<Tuple<T>>> crudResponse =
61+
readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))));
6062
return new TarantoolResponse<>(
61-
getRows(
62-
readData(
63-
response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))),
64-
getFormats(response));
63+
getRows(crudResponse),
64+
getFormats(response, crudResponse.getMetadata()));
6565
}
6666

6767
public static <T> CompletableFuture<SelectResponse<T>> convertSelectResultFuture(
@@ -82,9 +82,10 @@ public static <T> CompletableFuture<TarantoolResponse<T>> convertCrudSelectResul
8282

8383
public static <T> TarantoolResponse<T> readCrudSelectResult(
8484
IProtoResponse response, TypeReference<T> entity) {
85+
CrudResponse<T> crudResponse = readData(response, wrapIntoType(CrudResponse.class, entity));
8586
return new TarantoolResponse<>(
86-
getRows(readData(response, wrapIntoType(CrudResponse.class, entity))),
87-
getFormats(response));
87+
getRows(crudResponse),
88+
getFormats(response, crudResponse.getMetadata()));
8889
}
8990

9091
public static <T> TarantoolResponse<T> fromEventData(

tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithoutTargetType.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ private static Tuple<List<?>> getTupleWithInjectedFormat(
5656
}
5757
Integer formatId = tuple.getFormatId();
5858
List<Field> format = formats.get(formatId);
59+
if (formatId == null && formats.size() == 1) {
60+
format = formats.values().stream().findFirst().get();
61+
}
5962
tuple.setFormat(format);
6063
return tuple;
6164
}
@@ -72,9 +75,11 @@ public static CompletableFuture<Tuple<List<?>>> convertCrudSingleResultFuture(
7275

7376
public static TarantoolResponse<List<Tuple<List<?>>>> readCrudSingleResultData(
7477
IProtoResponse response) {
78+
CrudResponse<List<Tuple<List<?>>>> crudResponse =
79+
readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST));
7580
return new TarantoolResponse<>(
76-
getRows(readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST))),
77-
getFormats(response));
81+
getRows(crudResponse),
82+
getFormats(response, crudResponse.getMetadata()));
7883
}
7984

8085
public static <T> T getRows(CrudResponse<T> response) {
@@ -115,6 +120,37 @@ public static Map<Integer, List<Field>> getFormats(IProtoResponse response) {
115120
return formats;
116121
}
117122

123+
/**
124+
* Gets formats from IPROTO response or falls back to CRUD metadata.
125+
*
126+
* <p>This method first tries to get formats from IPROTO_TUPLE_FORMATS field.
127+
* If not available, it uses the CRUD metadata to construct the format map.
128+
* CRUD metadata is preferred when both are available.
129+
*
130+
* @param response the IPROTO response
131+
* @param crudMetadata the metadata from CRUD response (can be null)
132+
* @return map of format ID to list of fields
133+
*/
134+
public static Map<Integer, List<Field>> getFormats(
135+
IProtoResponse response, List<Field> crudMetadata) {
136+
// First try to get from IPROTO
137+
Map<Integer, List<Field>> formats = getFormats(response);
138+
139+
boolean formatsEmpty = formats == null
140+
|| formats.isEmpty()
141+
|| (formats.size() == 1 && formats.get(0) != null && formats.get(0).isEmpty());
142+
143+
boolean crudNotEmpty = crudMetadata != null && !crudMetadata.isEmpty();
144+
145+
// If IPROTO format is empty and we have CRUD metadata, use it
146+
if (formatsEmpty && crudNotEmpty) {
147+
// CRUD metadata doesn't have format ID, so we use 0 as default
148+
formats = Collections.singletonMap(0, crudMetadata);
149+
}
150+
151+
return formats;
152+
}
153+
118154
public static byte[] getPosition(IProtoResponse response) {
119155
byte[] position = null;
120156
ByteBodyValueWrapper rawPosition = response.getByteBodyValue(IPROTO_POSITION);
@@ -137,9 +173,11 @@ public static CompletableFuture<List<Tuple<List<?>>>> convertCrudSelectResultFut
137173

138174
public static TarantoolResponse<List<Tuple<List<?>>>> readCrudSelectResult(
139175
IProtoResponse response) {
176+
CrudResponse<List<Tuple<List<?>>>> crudResponse = readData(response,
177+
wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST));
140178
return new TarantoolResponse<>(
141-
getRows(readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST))),
142-
getFormats(response));
179+
getRows(crudResponse),
180+
getFormats(response, crudResponse.getMetadata()));
143181
}
144182

145183
private static List<Tuple<List<?>>> getTuplesWithInjectedFormat(

tarantool-shared-resources/vshard_cluster/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ ENV TARANTOOL_WORKDIR=$TARANTOOL_WORKDIR
88
WORKDIR $TARANTOOL_WORKDIR
99
COPY "cluster" "$TARANTOOL_WORKDIR"
1010

11+
RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirror.yandex.ru/ubuntu/|g; s|http://security.ubuntu.com/ubuntu/|http://mirror.yandex.ru/ubuntu/|g' /etc/apt/sources.list
12+
1113
# install dependencies
1214
RUN apt-get -y update && \
1315
apt-get -y install build-essential cmake make gcc git unzip cartridge-cli && \

0 commit comments

Comments
 (0)