|
41 | 41 | import org.apache.pulsar.client.api.SubscriptionType;
|
42 | 42 | import org.apache.pulsar.client.api.schema.Field;
|
43 | 43 | import org.apache.pulsar.client.api.schema.GenericRecord;
|
| 44 | +import org.apache.pulsar.client.impl.schema.generic.GenericJsonReader; |
| 45 | +import org.apache.pulsar.client.impl.schema.generic.GenericJsonRecord; |
44 | 46 | import org.apache.pulsar.common.schema.KeyValue;
|
45 | 47 | import org.apache.pulsar.common.schema.SchemaType;
|
46 | 48 | import org.apache.pulsar.shade.com.fasterxml.jackson.databind.JsonNode;
|
|
69 | 71 | import java.nio.charset.StandardCharsets;
|
70 | 72 | import java.time.Duration;
|
71 | 73 | import java.time.LocalDate;
|
72 |
| -import java.util.Collection; |
73 |
| -import java.util.HashMap; |
74 |
| -import java.util.Iterator; |
75 |
| -import java.util.List; |
76 |
| -import java.util.Locale; |
77 |
| -import java.util.Map; |
78 |
| -import java.util.Optional; |
79 |
| -import java.util.Set; |
| 74 | +import java.util.*; |
80 | 75 | import java.util.concurrent.Executors;
|
81 | 76 | import java.util.concurrent.TimeUnit;
|
82 | 77 | import java.util.concurrent.atomic.AtomicInteger;
|
@@ -196,6 +191,11 @@ public void testBatchInsert() throws InterruptedException, IOException {
|
196 | 191 | testBatchInsert("batchinsert");
|
197 | 192 | }
|
198 | 193 |
|
| 194 | + @Test |
| 195 | + public void testTimestampInCollection() throws InterruptedException, IOException { |
| 196 | + testTimestampInCollection("ks1"); |
| 197 | + } |
| 198 | + |
199 | 199 | void deployConnector(String ksName, String tableName) throws IOException, InterruptedException {
|
200 | 200 | String config = String.format(Locale.ROOT, "{\"%s\":\"%s\", \"%s\":\"%s\", \"%s\":\"%s\", \"%s\":\"%s\", \"%s\": \"%s\", \"%s\":\"%s\", \"%s\":\"%s\"}",
|
201 | 201 | CassandraSourceConnectorConfig.CONTACT_POINTS_OPT, "cassandra-1",
|
@@ -989,6 +989,73 @@ public void testBatchInsert(String ksName) throws InterruptedException, IOExcept
|
989 | 989 | }
|
990 | 990 | }
|
991 | 991 |
|
| 992 | + @SuppressWarnings("unchecked") |
| 993 | + public void testTimestampInCollection(String ksName) throws InterruptedException, IOException { |
| 994 | + try { |
| 995 | + try (CqlSession cqlSession = cassandraContainer1.getCqlSession()) { |
| 996 | + cqlSession.execute("CREATE KEYSPACE IF NOT EXISTS " + ksName + |
| 997 | + " WITH replication = {'class':'SimpleStrategy','replication_factor':'2'};"); |
| 998 | + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + ksName + ".table7 (a text, b timestamp, c list<timestamp>, d map<text, timestamp>, e set<timestamp>, PRIMARY KEY(a)) WITH cdc=true"); |
| 999 | + cqlSession.execute("INSERT INTO " + ksName + ".table7 (a,b,c,d,e) VALUES('1', '1990-04-04 08:52:01.581', ['1990-04-04 08:52:01.581', '1990-04-04 08:52:01.581'], {'key113606': '1990-04-04 08:52:01.581'}, {'1990-04-04 08:52:01.581', '1990-04-04 08:52:01.581'})"); |
| 1000 | + cqlSession.execute("INSERT INTO " + ksName + ".table7 (a,b,c,d,e) VALUES('2', '1999-11-07 05:30:00.780', ['1999-11-07 05:30:00.780', '1999-11-07 05:30:00.780'], {'key113606': '1999-11-07 05:30:00.780'}, {'1999-11-07 05:30:00.780'})"); |
| 1001 | + cqlSession.execute("INSERT INTO " + ksName + ".table7 (a,b,c,d,e) VALUES('3', '2025-07-17 18:02:01.871', ['2025-07-17 18:02:01.871', '2025-07-17 18:02:01.871'], {'key113606': '2025-07-17 18:02:01.871'}, {'2025-07-17 18:02:01.871', '2025-07-17 18:02:01.871', '2025-07-17 18:02:01.871'})"); |
| 1002 | + } |
| 1003 | + deployConnector(ksName, "table7"); |
| 1004 | + try (PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(pulsarContainer.getPulsarBrokerUrl()).build()) { |
| 1005 | + try (Consumer<GenericRecord> consumer = pulsarClient.newConsumer(org.apache.pulsar.client.api.Schema.AUTO_CONSUME()) |
| 1006 | + .topic(String.format(Locale.ROOT, "data-%s.table7", ksName)) |
| 1007 | + .subscriptionName("sub1") |
| 1008 | + .subscriptionType(SubscriptionType.Key_Shared) |
| 1009 | + .subscriptionMode(SubscriptionMode.Durable) |
| 1010 | + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) |
| 1011 | + .subscribe()) { |
| 1012 | + Message<GenericRecord> msg; |
| 1013 | + int receivedCount = 0; |
| 1014 | + List<Long> data = Arrays.asList(639219121581L, 941952600780L, 1752775321871L); |
| 1015 | + while ((msg = consumer.receive(60, TimeUnit.SECONDS)) != null && |
| 1016 | + receivedCount < 4) { |
| 1017 | + GenericRecord record = msg.getValue(); |
| 1018 | + assertEquals(this.schemaType, record.getSchemaType()); |
| 1019 | + Object key = getKey(msg); |
| 1020 | + GenericRecord value = getValue(record); |
| 1021 | + // assert key fields |
| 1022 | + assertEquals(Integer.toString(receivedCount+1) , getAndAssertKeyFieldAsString(key, "a")); |
| 1023 | + // assert value fields |
| 1024 | + assertEquals(data.get(receivedCount), value.getField("b")); |
| 1025 | + if (value.getField("c") instanceof GenericJsonRecord){ |
| 1026 | + JsonNode arrayNode = ((GenericJsonRecord) value.getField("c")).getJsonNode(); |
| 1027 | + assertTrue(arrayNode.isArray()); |
| 1028 | + assertEquals(2, arrayNode.size()); |
| 1029 | + assertEquals(data.get(receivedCount), arrayNode.get(0).asLong()); |
| 1030 | + assertEquals(data.get(receivedCount), arrayNode.get(1).asLong()); |
| 1031 | + } |
| 1032 | + else { |
| 1033 | + assertEquals(Arrays.asList(data.get(receivedCount), data.get(receivedCount)), value.getField("c")); |
| 1034 | + } |
| 1035 | + if (value instanceof GenericJsonRecord){ |
| 1036 | + JsonNode arrayNode = ((GenericJsonRecord) value.getField("e")).getJsonNode();; |
| 1037 | + assertTrue(arrayNode.isArray()); |
| 1038 | + assertEquals(1, arrayNode.size()); |
| 1039 | + assertEquals(data.get(receivedCount), arrayNode.get(0).asLong()); |
| 1040 | + } |
| 1041 | + else { |
| 1042 | + assertEquals(Collections.singletonList(data.get(receivedCount)), value.getField("e")); |
| 1043 | + } |
| 1044 | + |
| 1045 | + Map<String, Object> expectedMap = new HashMap<>(); |
| 1046 | + expectedMap.put("key113606", data.get(receivedCount)); |
| 1047 | + assertMapsEqual(expectedMap, value.getField("d")); |
| 1048 | + consumer.acknowledge(msg); |
| 1049 | + receivedCount++; |
| 1050 | + } |
| 1051 | + } |
| 1052 | + } |
| 1053 | + } finally { |
| 1054 | + dumpFunctionLogs("cassandra-source-" + ksName + "-table7"); |
| 1055 | + undeployConnector(ksName, "table7"); |
| 1056 | + } |
| 1057 | + } |
| 1058 | + |
992 | 1059 | @Test
|
993 | 1060 | public void testReadTimeout() throws InterruptedException, IOException {
|
994 | 1061 | final String ksName = "ksx";
|
@@ -1217,6 +1284,45 @@ private String getAndAssertKeyFieldAsString(Object key, String fieldName) {
|
1217 | 1284 | throw new RuntimeException("unknown key type " + key.getClass().getName());
|
1218 | 1285 | }
|
1219 | 1286 |
|
| 1287 | + @SuppressWarnings("unchecked") |
| 1288 | + private void assertMapsEqual(Map<String, Object> expected, Object actual) { |
| 1289 | + if (actual instanceof GenericJsonRecord) { |
| 1290 | + JsonNode node = ((GenericJsonRecord) actual).getJsonNode(); |
| 1291 | + assertEquals(expected.size(), node.size(), "Maps have different sizes"); |
| 1292 | + for (Map.Entry<String, Object> entry : expected.entrySet()) { |
| 1293 | + assertTrue(node.has(entry.getKey()), "Missing key: " + entry.getKey()); |
| 1294 | + assertEquals( |
| 1295 | + expected.get(entry.getKey()), |
| 1296 | + node.get(entry.getKey()).asLong(), |
| 1297 | + "Values differ for key: " + entry.getKey() |
| 1298 | + ); |
| 1299 | + } |
| 1300 | + } |
| 1301 | + else if (actual instanceof Map){ |
| 1302 | + Map<org.apache.pulsar.shade.org.apache.avro.util.Utf8, Object> actualMap = (Map<org.apache.pulsar.shade.org.apache.avro.util.Utf8, Object>) actual; |
| 1303 | + assertEquals(expected.size(), actualMap.size(), "Maps have different sizes"); |
| 1304 | + for (Map.Entry<String, Object> entry : expected.entrySet()) { |
| 1305 | + String expectedKey = entry.getKey(); |
| 1306 | + assertTrue(actualMap.keySet().stream() |
| 1307 | + .map(Utf8::toString) |
| 1308 | + .anyMatch(str -> str.equals(expectedKey)), "Missing key: " + expectedKey); |
| 1309 | + assertEquals( |
| 1310 | + expected.get(entry.getKey()), |
| 1311 | + actualMap.entrySet().stream() |
| 1312 | + .filter(e -> e.getKey().toString().equals(expectedKey)) |
| 1313 | + .findFirst() |
| 1314 | + .map(Map.Entry::getValue) |
| 1315 | + .orElse(null), |
| 1316 | + "Values differ for key: " + entry.getKey() |
| 1317 | + ); |
| 1318 | + } |
| 1319 | + } |
| 1320 | + else { |
| 1321 | + throw new RuntimeException("Unknown type of GenericRecord: " + actual.getClass().getName()); |
| 1322 | + } |
| 1323 | + |
| 1324 | + } |
| 1325 | + |
1220 | 1326 | private void assertKeyFieldIsNull(Object key, String fieldName) {
|
1221 | 1327 | if (key instanceof GenericRecord) {
|
1222 | 1328 | assertNull(((GenericRecord) key).getField(fieldName));
|
|
0 commit comments