/* * Copyright Debezium Authors. * * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ package io.debezium.connector.cassandra; import java.util.Objects; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.kafka.connect.data.Field; import org.apache.kafka.connect.data.Schema; import org.apache.kafka.connect.data.SchemaBuilder; import org.apache.kafka.connect.data.Struct; import com.datastax.driver.core.ColumnMetadata; import io.debezium.connector.cassandra.transforms.CassandraTypeConverter; import io.debezium.connector.cassandra.transforms.CassandraTypeDeserializer; /** * Cell-level data about the source event. Each cell contains the name, value and * type of a column in a Cassandra table. */ public class CellData implements KafkaRecord { /** * The type of a column in a Cassandra table */ public enum ColumnType { /** * A partition column is responsible for data distribution across nodes for this table. * Every Cassandra table must have at least one partition column. */ PARTITION, /** * A clustering column is used to specifies the order that the data is arranged inside the partition. * A Cassandra table may not have any clustering column, */ CLUSTERING, /** * A regular column is a column that is not a partition or a clustering column. */ REGULAR } public static final String CELL_VALUE_KEY = "value"; public static final String CELL_DELETION_TS_KEY = "deletion_ts"; public static final String CELL_SET_KEY = "set"; public final String name; public final Object value; public final Object deletionTs; public final ColumnType columnType; public CellData(String name, Object value, Object deletionTs, ColumnType columnType) { this.name = name; this.value = value; this.deletionTs = deletionTs; this.columnType = columnType; } public boolean isPrimary() { return columnType == ColumnType.PARTITION || columnType == ColumnType.CLUSTERING; } @Override public Struct record(Schema schema) { Struct cellStruct = new Struct(schema) .put(CELL_DELETION_TS_KEY, deletionTs) .put(CELL_SET_KEY, true); if (value instanceof Struct) { Schema valueSchema = schema.field(CELL_VALUE_KEY).schema(); Struct clonedValue = cloneValue(valueSchema, (Struct) value); cellStruct.put(CELL_VALUE_KEY, clonedValue); } else { cellStruct.put(CELL_VALUE_KEY, value); } return cellStruct; } // Encountered DataException("Struct schemas do not match.") when value is a Struct. // The error is because the valueSchema is optional, but the schema of value formed during deserialization is not. // This is a temporary workaround to fix this problem. private Struct cloneValue(Schema valueSchema, Struct value) { Struct clonedValue = new Struct(valueSchema); for (Field field : valueSchema.fields()) { String fieldName = field.name(); clonedValue.put(fieldName, value.get(fieldName)); } return clonedValue; } static Schema cellSchema(ColumnMetadata cm, boolean optional) { AbstractType<?> convertedType = CassandraTypeConverter.convert(cm.getType()); Schema valueSchema = CassandraTypeDeserializer.getSchemaBuilder(convertedType).optional().build(); if (valueSchema != null) { SchemaBuilder schemaBuilder = SchemaBuilder.struct().name(cm.getName()) .field(CELL_VALUE_KEY, valueSchema) .field(CELL_DELETION_TS_KEY, Schema.OPTIONAL_INT64_SCHEMA) .field(CELL_SET_KEY, Schema.BOOLEAN_SCHEMA); if (optional) { schemaBuilder.optional(); } return schemaBuilder.build(); } else { return null; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } CellData that = (CellData) o; return Objects.equals(name, that.name) && Objects.equals(value, that.value) && deletionTs == that.deletionTs && columnType == that.columnType; } @Override public int hashCode() { return Objects.hash(name, value, deletionTs, columnType); } @Override public String toString() { return "{" + "name=" + name + ", value=" + value + ", deletionTs=" + deletionTs + ", type=" + columnType.name() + '}'; } }