From faca7929a0f6e5a166c6d9ef6be9b213aa0acfed Mon Sep 17 00:00:00 2001 From: xuchuan Date: Fri, 11 Nov 2022 19:03:31 +0800 Subject: [PATCH] enhance(datastore): add tuple type support (#1484) --- .../starwhale/mlops/datastore/ColumnType.java | 10 +- .../mlops/datastore/ColumnTypeList.java | 2 +- .../mlops/datastore/ColumnTypeScalar.java | 3 +- .../mlops/datastore/ColumnTypeTuple.java | 42 ++++++ .../starwhale/mlops/datastore/RecordList.java | 2 + .../mlops/datastore/ColumnTypeListTest.java | 3 + .../mlops/datastore/ColumnTypeTest.java | 76 ++++++++++ .../mlops/datastore/ColumnTypeTupleTest.java | 142 ++++++++++++++++++ .../mlops/datastore/DataStoreTest.java | 97 ++++++++++++ .../datastore/impl/MemoryTableImplTest.java | 10 +- 10 files changed, 380 insertions(+), 7 deletions(-) create mode 100644 server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeTuple.java create mode 100644 server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTupleTest.java diff --git a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnType.java b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnType.java index a3d143ef14..9a2f4b4d26 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnType.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnType.java @@ -38,12 +38,16 @@ public abstract class ColumnType { public static ColumnType fromColumnSchemaDesc(ColumnSchemaDesc schema) { var typeName = schema.getType().toUpperCase(); - if (typeName.equals(ColumnTypeList.TYPE_NAME)) { + if (typeName.equals(ColumnTypeList.TYPE_NAME) || typeName.equals(ColumnTypeTuple.TYPE_NAME)) { var elementType = schema.getElementType(); if (elementType == null) { - throw new IllegalArgumentException("elementType should not be null for LIST"); + throw new IllegalArgumentException("elementType should not be null for " + typeName); + } + if (typeName.equals(ColumnTypeList.TYPE_NAME)) { + return new ColumnTypeList(ColumnType.fromColumnSchemaDesc(elementType)); + } else { + return new ColumnTypeTuple(ColumnType.fromColumnSchemaDesc(elementType)); } - return new ColumnTypeList(ColumnType.fromColumnSchemaDesc(elementType)); } else if (typeName.equals(ColumnTypeObject.TYPE_NAME)) { var attributes = schema.getAttributes(); if (attributes == null || attributes.isEmpty()) { diff --git a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeList.java b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeList.java index a82f632c1f..edd49eafb5 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeList.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeList.java @@ -38,7 +38,7 @@ public class ColumnTypeList extends ColumnType { public static final String TYPE_NAME = "LIST"; - private final ColumnType elementType; + protected final ColumnType elementType; ColumnTypeList(ColumnType elementType) { this.elementType = elementType; diff --git a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeScalar.java b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeScalar.java index bde9035e0c..dda7e89ff7 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeScalar.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeScalar.java @@ -153,7 +153,8 @@ public Object encode(Object value, boolean rawResult) { } else if (this == STRING) { return value; } else if (this == BYTES) { - return Base64.getEncoder().encodeToString(((ByteBuffer) value).array()); + var base64 = Base64.getEncoder().encode(((ByteBuffer) value).duplicate()); + return StandardCharsets.UTF_8.decode(base64).toString(); } } throw new IllegalArgumentException("invalid type " + this); diff --git a/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeTuple.java b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeTuple.java new file mode 100644 index 0000000000..88374c128c --- /dev/null +++ b/server/controller/src/main/java/ai/starwhale/mlops/datastore/ColumnTypeTuple.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Starwhale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.starwhale.mlops.datastore; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class ColumnTypeTuple extends ColumnTypeList { + + public static final String TYPE_NAME = "TUPLE"; + + ColumnTypeTuple(ColumnType elementType) { + super(elementType); + } + + @Override + public String toString() { + return "(" + elementType + ")"; + } + + + @Override + public String getTypeName() { + return ColumnTypeTuple.TYPE_NAME; + } +} diff --git a/server/controller/src/main/java/ai/starwhale/mlops/datastore/RecordList.java b/server/controller/src/main/java/ai/starwhale/mlops/datastore/RecordList.java index d065342a76..37969b0196 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/datastore/RecordList.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/datastore/RecordList.java @@ -19,10 +19,12 @@ import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; @Getter @AllArgsConstructor +@EqualsAndHashCode public class RecordList { private Map columnTypeMap; diff --git a/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeListTest.java b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeListTest.java index f4bc94ceb6..c0ddc2c7b8 100644 --- a/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeListTest.java +++ b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeListTest.java @@ -65,6 +65,9 @@ public void testIsComparableWith() { assertThat(new ColumnTypeList(ColumnTypeScalar.INT32).isComparableWith( new ColumnTypeList(ColumnTypeScalar.STRING)), is(false)); + assertThat(new ColumnTypeList(ColumnTypeScalar.INT32).isComparableWith( + new ColumnTypeTuple(ColumnTypeScalar.INT32)), + is(true)); } @Test diff --git a/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTest.java b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTest.java index a11e17029e..94ba607e2b 100644 --- a/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTest.java +++ b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTest.java @@ -49,6 +49,19 @@ public void testFromColumnSchemaDesc() { .build()) .build()), is(new ColumnTypeList(new ColumnTypeList(ColumnTypeScalar.INT32)))); + assertThat("simple tuple", ColumnType.fromColumnSchemaDesc(ColumnSchemaDesc.builder() + .type("TUPLE") + .elementType(ColumnSchemaDesc.builder().type("INT32").build()) + .build()), + is(new ColumnTypeTuple(ColumnTypeScalar.INT32))); + assertThat("composite tuple", ColumnType.fromColumnSchemaDesc(ColumnSchemaDesc.builder() + .type("TUPLE") + .elementType(ColumnSchemaDesc.builder() + .type("LIST") + .elementType(ColumnSchemaDesc.builder().type("INT32").build()) + .build()) + .build()), + is(new ColumnTypeTuple(new ColumnTypeList(ColumnTypeScalar.INT32)))); assertThat("object", ColumnType.fromColumnSchemaDesc(ColumnSchemaDesc.builder() .type("OBJECT") .pythonType("t") @@ -380,6 +393,69 @@ public void testCompareList() { greaterThan(0)); } + @Test + public void testCompareTuple() { + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 4)), + lessThan(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 3, 0)), + lessThan(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 3)), + equalTo(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 2)), + greaterThan(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of()), + greaterThan(0)); + } + + @Test + public void testCompareListTuple() { + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeList(ColumnTypeScalar.INT8), + List.of(1, 2, 4)), + lessThan(0)); + assertThat(ColumnType.compare(new ColumnTypeList(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 4)), + lessThan(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeList(ColumnTypeScalar.INT8), + List.of(1, 2, 3)), + equalTo(0)); + assertThat(ColumnType.compare(new ColumnTypeList(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 3)), + equalTo(0)); + assertThat(ColumnType.compare(new ColumnTypeTuple(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeList(ColumnTypeScalar.INT8), + List.of(1, 2, 2)), + greaterThan(0)); + assertThat(ColumnType.compare(new ColumnTypeList(ColumnTypeScalar.INT32), + List.of(1, 2, 3), + new ColumnTypeTuple(ColumnTypeScalar.INT8), + List.of(1, 2, 2)), + greaterThan(0)); + } + @Test public void testCompareObject() { var type = new ColumnTypeObject("t", diff --git a/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTupleTest.java b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTupleTest.java new file mode 100644 index 0000000000..fcc93bbe12 --- /dev/null +++ b/server/controller/src/test/java/ai/starwhale/mlops/datastore/ColumnTypeTupleTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2022 Starwhale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.starwhale.mlops.datastore; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import ai.starwhale.mlops.exception.SwValidationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ColumnTypeTupleTest { + + @Test + public void testGetTypeName() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).getTypeName(), is("TUPLE")); + } + + @Test + public void testToColumnSchemaDesc() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).toColumnSchemaDesc("t"), + is(ColumnSchemaDesc.builder() + .name("t") + .type("TUPLE") + .elementType(ColumnSchemaDesc.builder() + .type("INT32") + .build()) + .build())); + } + + @Test + public void testToString() { + assertThat(new ColumnTypeList(ColumnTypeScalar.INT32).toString(), is("[INT32]")); + } + + + @Test + public void testIsComparableWith() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith(ColumnTypeScalar.UNKNOWN), is(true)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith(ColumnTypeScalar.INT32), is(false)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith( + new ColumnTypeTuple(ColumnTypeScalar.INT32)), + is(true)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith( + new ColumnTypeTuple(ColumnTypeScalar.FLOAT64)), + is(true)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith( + new ColumnTypeTuple(ColumnTypeScalar.STRING)), + is(false)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).isComparableWith( + new ColumnTypeList(ColumnTypeScalar.INT32)), + is(true)); + } + + @Test + public void testEncode() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).encode(List.of(9, 10, 11), false), + is(List.of("00000009", "0000000a", "0000000b"))); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).encode(List.of(9, 10, 11), true), + is(List.of("9", "10", "11"))); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).encode(new ArrayList() { + { + add(0); + add(null); + add(1); + } + }, false), + is(new ArrayList() { + { + add("00000000"); + add(null); + add("00000001"); + } + })); + var composite = new ColumnTypeTuple( + new ColumnTypeObject("t", Map.of("a", ColumnTypeScalar.INT32, "b", ColumnTypeScalar.INT32))); + assertThat(composite.encode(List.of(Map.of("a", 9, "b", 10), Map.of("a", 10, "b", 11)), false), + is(List.of(Map.of("a", "00000009", "b", "0000000a"), Map.of("a", "0000000a", "b", "0000000b")))); + assertThat(composite.encode(List.of(Map.of("a", 9, "b", 10), Map.of("a", 10, "b", 11)), true), + is(List.of(Map.of("a", "9", "b", "10"), Map.of("a", "10", "b", "11")))); + } + + @Test + public void testDecode() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).decode(List.of("9", "a", "b")), + is(List.of(9, 10, 11))); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).decode(new ArrayList() { + { + add("0"); + add(null); + add("1"); + } + }), + is(new ArrayList() { + { + add(0); + add(null); + add(1); + } + })); + var composite = new ColumnTypeTuple( + new ColumnTypeObject("t", Map.of("a", ColumnTypeScalar.INT32, "b", ColumnTypeScalar.INT32))); + assertThat(composite.decode(List.of(Map.of("a", "9", "b", "a"), Map.of("a", "a", "b", "b"))), + is(List.of(Map.of("a", 9, "b", 10), Map.of("a", 10, "b", 11)))); + + assertThrows(SwValidationException.class, () -> new ColumnTypeTuple(ColumnTypeScalar.INT32).decode("9")); + assertThrows(SwValidationException.class, + () -> new ColumnTypeTuple(ColumnTypeScalar.INT32).decode(List.of("z"))); + } + + @Test + public void testFromAndToWal() { + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).toWal(-1, List.of(9, 10, 11)).getIndex(), is(-1)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).toWal(10, List.of(9, 10, 11)).getIndex(), is(10)); + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).fromWal( + new ColumnTypeTuple(ColumnTypeScalar.INT32).toWal(0, null).build()), + nullValue()); + + assertThat(new ColumnTypeTuple(ColumnTypeScalar.INT32).fromWal( + new ColumnTypeTuple(ColumnTypeScalar.INT32).toWal(0, List.of(9, 10, 11)).build()), + is(List.of(9, 10, 11))); + } + +} diff --git a/server/controller/src/test/java/ai/starwhale/mlops/datastore/DataStoreTest.java b/server/controller/src/test/java/ai/starwhale/mlops/datastore/DataStoreTest.java index bf9618eb4d..c5ecff3515 100644 --- a/server/controller/src/test/java/ai/starwhale/mlops/datastore/DataStoreTest.java +++ b/server/controller/src/test/java/ai/starwhale/mlops/datastore/DataStoreTest.java @@ -32,8 +32,10 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -936,4 +938,99 @@ public void execute() throws Exception { assertThat(result, is(inserted.get(i).stream().sorted().collect(Collectors.toList()))); } } + + @Test + public void testAllTypes() throws Exception { + Map record = new HashMap<>() { + { + put("key", "x"); + put("a", "1"); + put("b", "10"); + put("c", "1000"); + put("d", "00100000"); + put("e", "0000000010000000"); + put("f", Integer.toHexString(Float.floatToIntBits(1.1f))); + put("g", Long.toHexString(Double.doubleToLongBits(1.1))); + put("h", Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8))); + put("i", null); + put("j", List.of("0000000a")); + put("k", Map.of("a", "0000000b", "b", "0000000c")); + put("l", List.of("0000000b")); + put("complex", Map.of("a", List.of(List.of("00000001")), "b", List.of(List.of("00000002")))); + } + }; + var nullRecord = new HashMap(); + record.forEach((k, v) -> { + if (k.equals("key")) { + nullRecord.put(k, "y"); + } else { + nullRecord.put(k, null); + } + }); + var columnTypeMap = new HashMap() { + { + put("key", ColumnTypeScalar.STRING); + put("a", ColumnTypeScalar.BOOL); + put("b", ColumnTypeScalar.INT8); + put("c", ColumnTypeScalar.INT16); + put("d", ColumnTypeScalar.INT32); + put("e", ColumnTypeScalar.INT64); + put("f", ColumnTypeScalar.FLOAT32); + put("g", ColumnTypeScalar.FLOAT64); + put("h", ColumnTypeScalar.BYTES); + put("i", ColumnTypeScalar.UNKNOWN); + put("j", new ColumnTypeList(ColumnTypeScalar.INT32)); + put("k", new ColumnTypeObject("t", Map.of("a", ColumnTypeScalar.INT32, "b", ColumnTypeScalar.INT32))); + put("l", new ColumnTypeTuple(ColumnTypeScalar.INT32)); + put("complex", new ColumnTypeObject("tt", Map.of( + "a", new ColumnTypeList(new ColumnTypeTuple(ColumnTypeScalar.INT32)), + "b", new ColumnTypeTuple(new ColumnTypeList(ColumnTypeScalar.INT32)) + ))); + } + }; + var expected = new RecordList(columnTypeMap, List.of(record, nullRecord), "y"); + this.dataStore.update("t", + new TableSchemaDesc("key", + columnTypeMap.entrySet().stream() + .map(entry -> entry.getValue().toColumnSchemaDesc(entry.getKey())) + .collect(Collectors.toList())), + List.of(record, nullRecord)); + assertThat(this.dataStore.scan(DataStoreScanRequest.builder() + .tables(List.of(DataStoreScanRequest.TableInfo.builder() + .tableName("t") + .keepNone(true) + .build())) + .keepNone(true) + .build()), + is(expected)); + + // check WAL + this.dataStore.terminate(); + this.createDateStore(DataStoreParams.builder() + .dumpInterval("1s") + .minNoUpdatePeriod("1ms") + .build()); + assertThat(this.dataStore.scan(DataStoreScanRequest.builder() + .tables(List.of(DataStoreScanRequest.TableInfo.builder() + .tableName("t") + .keepNone(true) + .build())) + .keepNone(true) + .build()), + is(expected)); + // check parquet + while (this.dataStore.hasDirtyTables()) { + Thread.sleep(100); + } + this.dataStore.terminate(); + this.createDateStore(DataStoreParams.builder().build()); + assertThat(this.dataStore.scan(DataStoreScanRequest.builder() + .tables(List.of(DataStoreScanRequest.TableInfo.builder() + .tableName("t") + .keepNone(true) + .build())) + .keepNone(true) + .build()), + is(expected)); + } } diff --git a/server/controller/src/test/java/ai/starwhale/mlops/datastore/impl/MemoryTableImplTest.java b/server/controller/src/test/java/ai/starwhale/mlops/datastore/impl/MemoryTableImplTest.java index db536401df..f6945646a1 100644 --- a/server/controller/src/test/java/ai/starwhale/mlops/datastore/impl/MemoryTableImplTest.java +++ b/server/controller/src/test/java/ai/starwhale/mlops/datastore/impl/MemoryTableImplTest.java @@ -281,7 +281,11 @@ public void testUpdateAllColumnTypes() { .attributes(List.of(ColumnSchemaDesc.builder().name("a").type("INT32").build(), ColumnSchemaDesc.builder().name("b").type("INT32").build())) .build(), - ColumnSchemaDesc.builder().name("l").type("FLOAT64").build())), + ColumnSchemaDesc.builder().name("l").type("FLOAT64").build(), + ColumnSchemaDesc.builder().name("m") + .type("TUPLE") + .elementType(ColumnSchemaDesc.builder().type("INT32").build()) + .build())), List.of(new HashMap<>() { { @@ -298,11 +302,12 @@ public void testUpdateAllColumnTypes() { put("j", List.of("a")); put("k", Map.of("a", "b", "b", "c")); put("l", Long.toHexString(Double.doubleToLongBits(0.0))); + put("m", List.of("b")); } })); assertThat("all types", scanAll(this.memoryTable, - List.of("key", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"), + List.of("key", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"), false), contains(new MemoryTable.RecordResult("x", new HashMap<>() { @@ -319,6 +324,7 @@ public void testUpdateAllColumnTypes() { put("j", List.of(10)); put("k", Map.of("a", 11, "b", 12)); put("l", 0.0); + put("m", List.of(11)); } }))); }