diff --git a/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ES8xTemplate.java b/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ES8xTemplate.java index 7f125b7e..90225747 100644 --- a/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ES8xTemplate.java +++ b/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ES8xTemplate.java @@ -65,8 +65,9 @@ public class ES8xTemplate implements ESTemplate { if (mapping.getId() != null) { String parentVal = (String) esFieldData.remove("$parent_routing"); if (mapping.isUpsert()) { - ESUpdateRequest updateRequest = esConnection.new ES8xUpdateRequest(mapping.getIndex(), - pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true); + ESUpdateRequest updateRequest = esConnection.new ES8xUpdateRequest(mapping.getIndex(), pkVal.toString()) + .setDoc(esFieldData) + .setDocAsUpsert(true); if (StringUtils.isNotEmpty(parentVal)) { updateRequest.setRouting(parentVal); } diff --git a/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ESConnection.java b/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ESConnection.java index 429542d7..af2bbdc8 100644 --- a/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ESConnection.java +++ b/client-adapter/es8x/src/main/java/com/alibaba/otter/canal/client/adapter/es8x/support/ESConnection.java @@ -1,18 +1,29 @@ package com.alibaba.otter.canal.client.adapter.es8x.support; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; import java.util.Arrays; import java.util.Map; +import javax.net.ssl.SSLContext; + import org.apache.commons.lang.StringUtils; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkRequestBuilder; @@ -51,6 +62,47 @@ public class ESConnection { private RestHighLevelClient restHighLevelClient; public ESConnection(String[] hosts, Map properties) throws UnknownHostException{ + String caPath = properties.get("security.ca.path"); + if (StringUtils.isNotEmpty(caPath)) { + connectEsWithCa(hosts, properties, caPath); + } else { + connectEsWithoutCa(hosts, properties); + } + } + + private void connectEsWithCa(String[] hosts, Map properties, String caPath) { + Path caCertificatePath = Paths.get(caPath); + try (InputStream is = Files.newInputStream(caCertificatePath)) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + Certificate trustedCa = factory.generateCertificate(is); + KeyStore trustStore = KeyStore.getInstance("pkcs12"); + trustStore.load(null, null); + trustStore.setCertificateEntry("ca", trustedCa); + SSLContextBuilder sslContextBuilder = SSLContexts.custom().loadTrustMaterial(trustStore, null); + final SSLContext sslContext = sslContextBuilder.build(); + + HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new); + RestClientBuilder restClientBuilder = RestClient.builder(httpHosts); + String nameAndPwd = properties.get("security.auth"); + if (StringUtils.isNotEmpty(nameAndPwd) && nameAndPwd.contains(":")) { + String[] nameAndPwdArr = nameAndPwd.split(":"); + final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(nameAndPwdArr[0], nameAndPwdArr[1])); + restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> { + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + return httpClientBuilder.setSSLContext(sslContext); + }); + } + restHighLevelClient = new RestHighLevelClientBuilder(restClientBuilder.build()) + .setApiCompatibilityMode(true) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void connectEsWithoutCa(String[] hosts, Map properties) { HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new); RestClientBuilder restClientBuilder = RestClient.builder(httpHosts); String nameAndPwd = properties.get("security.auth"); @@ -280,9 +332,7 @@ public class ESConnection { private BulkRequest bulkRequest; public ES8xBulkRequest(){ - bulkRequest = new BulkRequest(); - } public void resetBulk() { diff --git a/client-adapter/launcher/src/main/resources/application.yml b/client-adapter/launcher/src/main/resources/application.yml index 822330f6..4e260a71 100644 --- a/client-adapter/launcher/src/main/resources/application.yml +++ b/client-adapter/launcher/src/main/resources/application.yml @@ -91,6 +91,7 @@ canal.conf: # hosts: 127.0.0.1:9300 # 127.0.0.1:9200 for rest mode # properties: # mode: transport # or rest +# # security.ca.path: /etc/es8/ca.crt # # security.auth: test:123456 # only used for rest mode # cluster.name: elasticsearch # - name: kudu @@ -113,4 +114,4 @@ canal.conf: # jdbc.password: 123456 # batchSize: 3000 # scheduleTime: 600 # second unit -# threads: 3 # parallel threads \ No newline at end of file +# threads: 3 # parallel threads diff --git a/client/src/main/java/com/alibaba/otter/canal/client/pulsarmq/PulsarMQCanalConnector.java b/client/src/main/java/com/alibaba/otter/canal/client/pulsarmq/PulsarMQCanalConnector.java index 89358bde..cc3e4f51 100644 --- a/client/src/main/java/com/alibaba/otter/canal/client/pulsarmq/PulsarMQCanalConnector.java +++ b/client/src/main/java/com/alibaba/otter/canal/client/pulsarmq/PulsarMQCanalConnector.java @@ -25,91 +25,98 @@ import com.google.common.collect.Lists; * 注意点: * 1. 相比于canal {@linkplain SimpleCanalConnector}, 这里get和ack操作不能有并发, 必须是一个线程执行get后,内存里执行完毕ack后再取下一个get * + * * todo 重复消费的概率相当高。一次批处理中,只要有一个消息处理失败,则该批次全部重试 * * @since 1.1.1 */ public class PulsarMQCanalConnector implements CanalMQConnector { - private static final Logger logger = LoggerFactory.getLogger(PulsarMQCanalConnector.class); + private static final Logger logger = LoggerFactory + .getLogger(PulsarMQCanalConnector.class); private volatile Messages lastGetBatchMessage; /** * 连接pulsar客户端 */ - private PulsarClient pulsarClient; + private PulsarClient pulsarClient; /** * 消费者 */ - private Consumer consumer; + private Consumer consumer; /** * 是否扁平化Canal消息内容 */ - private boolean isFlatMessage = false; + private boolean isFlatMessage = false; /** * 主题名称 */ - private String topic; + private String topic; /** * 环境连接URL */ - private String serviceUrl; + private String serviceUrl; /** * 角色认证token */ - private String roleToken; + private String roleToken; + + /** + * listener name + */ + private String listenerName; + /** * 订阅客户端名称 */ - private String subscriptName; + private String subscriptName; /** * 每次批量获取数据的最大条目数,默认30 */ - private int batchSize = 30; + private int batchSize = 30; /** - * 与{@code batchSize}一起决定批量获取的数据大小 - * 当: + * 与{@code batchSize}一起决定批量获取的数据大小 当: *

* 1. {@code batchSize} 条消息未消费时
* 2. 距上一次批量消费时间达到{@code batchTimeoutSeconds}秒时 *

* 任一条件满足,即执行批量消费 */ - private int getBatchTimeoutSeconds = 30; + private int getBatchTimeoutSeconds = 30; /** * 批量处理消息时,一次批量处理的超时时间秒数 *

* 该时间应该根据{@code batchSize}和{@code batchTimeoutSeconds}合理设置 *

*/ - private int batchProcessTimeoutSeconds = 60; + private int batchProcessTimeoutSeconds = 60; /** * 消费失败后的重试秒数,默认60秒 */ - private int redeliveryDelaySeconds = 60; + private int redeliveryDelaySeconds = 60; /** * 当客户端接收到消息,30秒还没有返回ack给服务端时,ack超时,会重新消费该消息 */ - private int ackTimeoutSeconds = 30; + private int ackTimeoutSeconds = 30; /** * 是否开启消息失败重试功能,默认开启 */ - private boolean isRetry = true; + private boolean isRetry = true; /** *

* true重试(-RETRY)和死信队列(-DLQ)后缀为大写,有些地方创建的为小写,需确保正确 *

*/ - private boolean isRetryDLQUpperCase = false; + private boolean isRetryDLQUpperCase = false; /** * 最大重试次数 */ - private int maxRedeliveryCount = 128; + private int maxRedeliveryCount = 128; /** * 连接标识位,在连接或关闭连接后改变值 */ - private boolean connected = false; + private boolean connected = false; /** * 除必要参数外,其他参数使用默认值 @@ -119,16 +126,21 @@ public class PulsarMQCanalConnector implements CanalMQConnector { *

* * @param isFlatMessage true使用扁平消息 - * @param serviceUrl pulsar服务连接地址,通常为:pulsar:host:ip或http://host:ip - * @param roleToken 有对应topic的消费者权限的角色token - * @param topic 订阅主题 + * @param serviceUrl pulsar服务连接地址,通常为:pulsar:host:ip或http://host:ip + * @param roleToken 有对应topic的消费者权限的角色token + * @param topic 订阅主题 * @param subscriptName 订阅和客户端名称,同一个订阅名视为同一个消费实例 * @date 2021/9/18 08:54 * @author chad * @since 1 by chad at 2021/9/18 完善 */ - public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic - , String subscriptName) { + public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic, + String subscriptName){ + this(isFlatMessage, serviceUrl, roleToken, topic, subscriptName, null); + } + + public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic, + String subscriptName, String listenerName){ this.isFlatMessage = isFlatMessage; this.serviceUrl = serviceUrl; this.roleToken = roleToken; @@ -137,6 +149,7 @@ public class PulsarMQCanalConnector implements CanalMQConnector { if (StringUtils.isEmpty(this.subscriptName)) { throw new RuntimeException("Pulsar Consumer subscriptName required"); } + this.listenerName = listenerName; } /** @@ -146,10 +159,31 @@ public class PulsarMQCanalConnector implements CanalMQConnector { * @author chad * @since 1 by chad at 2021/9/18 完善 */ - public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic - , String subscriptName, int batchSize, int getBatchTimeoutSeconds, int batchProcessTimeoutSeconds - , int redeliveryDelaySeconds, int ackTimeoutSeconds, boolean isRetry, boolean isRetryDLQUpperCase - , int maxRedeliveryCount) { + public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic, + String subscriptName, int batchSize, int getBatchTimeoutSeconds, + int batchProcessTimeoutSeconds, int redeliveryDelaySeconds, int ackTimeoutSeconds, + boolean isRetry, boolean isRetryDLQUpperCase, int maxRedeliveryCount){ + this(isFlatMessage, + serviceUrl, + roleToken, + topic, + subscriptName, + batchSize, + getBatchTimeoutSeconds, + batchProcessTimeoutSeconds, + redeliveryDelaySeconds, + ackTimeoutSeconds, + isRetry, + isRetryDLQUpperCase, + maxRedeliveryCount, + null); + } + + public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic, + String subscriptName, int batchSize, int getBatchTimeoutSeconds, + int batchProcessTimeoutSeconds, int redeliveryDelaySeconds, int ackTimeoutSeconds, + boolean isRetry, boolean isRetryDLQUpperCase, int maxRedeliveryCount, + String listenerName){ this.isFlatMessage = isFlatMessage; this.serviceUrl = serviceUrl; this.roleToken = roleToken; @@ -166,16 +200,20 @@ public class PulsarMQCanalConnector implements CanalMQConnector { this.isRetry = isRetry; this.isRetryDLQUpperCase = isRetryDLQUpperCase; this.maxRedeliveryCount = maxRedeliveryCount; + this.listenerName = listenerName; } @Override public void connect() throws CanalClientException { // 连接创建客户端 try { - pulsarClient = PulsarClient.builder() - .serviceUrl(serviceUrl) - .authentication(AuthenticationFactory.token(roleToken)) - .build(); + ClientBuilder builder = PulsarClient.builder() + .serviceUrl(serviceUrl) + .authentication(AuthenticationFactory.token(roleToken)); + if (StringUtils.isNotEmpty(listenerName)) { + builder.listenerName(listenerName); + } + pulsarClient = builder.build(); } catch (PulsarClientException e) { throw new RuntimeException(e); } @@ -224,15 +262,14 @@ public class PulsarMQCanalConnector implements CanalMQConnector { builder.subscriptionType(SubscriptionType.Failover); builder - // 调用consumer.negativeAcknowledge(message) (即nack)来表示消费失败的消息 - // 在指定的时间进行重新消费,默认是1分钟。 - .negativeAckRedeliveryDelay(this.redeliveryDelaySeconds, TimeUnit.SECONDS) - .subscriptionName(this.subscriptName) - ; + // 调用consumer.negativeAcknowledge(message) (即nack)来表示消费失败的消息 + // 在指定的时间进行重新消费,默认是1分钟。 + .negativeAckRedeliveryDelay(this.redeliveryDelaySeconds, TimeUnit.SECONDS) + .subscriptionName(this.subscriptName); if (this.isRetry) { DeadLetterPolicy.DeadLetterPolicyBuilder dlqBuilder = DeadLetterPolicy.builder() - // 最大重试次数 - .maxRedeliverCount(this.maxRedeliveryCount); + // 最大重试次数 + .maxRedeliverCount(this.maxRedeliveryCount); // 指定重试队列,不是多个或通配符topic才能判断重试队列 if (!MQUtil.isPatternTag(this.topic)) { String retryTopic = this.topic + (this.isRetryDLQUpperCase ? "-RETRY" : "-retry"); @@ -241,19 +278,17 @@ public class PulsarMQCanalConnector implements CanalMQConnector { dlqBuilder.deadLetterTopic(dlqTopic); } - //默认关闭,如果需要重试则开启 - builder.enableRetry(true) - .deadLetterPolicy(dlqBuilder.build()); + // 默认关闭,如果需要重试则开启 + builder.enableRetry(true).deadLetterPolicy(dlqBuilder.build()); } // ack超时 builder.ackTimeout(this.ackTimeoutSeconds, TimeUnit.SECONDS); // pulsar批量获取消息设置 - builder.batchReceivePolicy(new BatchReceivePolicy.Builder() - .maxNumMessages(this.batchSize) - .timeout(this.getBatchTimeoutSeconds, TimeUnit.SECONDS) - .build()); + builder.batchReceivePolicy(new BatchReceivePolicy.Builder().maxNumMessages(this.batchSize) + .timeout(this.getBatchTimeoutSeconds, TimeUnit.SECONDS) + .build()); try { this.consumer = builder.subscribe(); @@ -263,7 +298,6 @@ public class PulsarMQCanalConnector implements CanalMQConnector { } } - @Override public void subscribe() throws CanalClientException { this.subscribe(null); @@ -284,7 +318,7 @@ public class PulsarMQCanalConnector implements CanalMQConnector { * 不关注业务执行结果,只要收到消息即认识消费成功,自动ack * * @param timeout 阻塞获取消息的超时时间 - * @param unit 时间单位 + * @param unit 时间单位 * @return java.util.List * @date 2021/9/13 22:24 * @author chad @@ -300,13 +334,14 @@ public class PulsarMQCanalConnector implements CanalMQConnector { } /** - * 关心业务执行结果,业务侧根据执行结果调用 {@link PulsarMQCanalConnector#ack()}或{@link PulsarMQCanalConnector#rollback()} + * 关心业务执行结果,业务侧根据执行结果调用 + * {@link PulsarMQCanalConnector#ack()}或{@link PulsarMQCanalConnector#rollback()} *

* 本方法示支持多线程,在MQ保障顺序的前提下,也无法提供单Topic多线程 *

* * @param timeout 阻塞获取消息的超时时间 - * @param unit 时间单位 + * @param unit 时间单位 * @return java.util.List * @date 2021/9/13 22:26 * @author chad @@ -340,7 +375,8 @@ public class PulsarMQCanalConnector implements CanalMQConnector { * @return java.util.List * @date 2021/9/14 15:20 * @author chad - * @since 1 by chad at 2021/9/14 供{@link PulsarMQCanalConnector#getListWithoutAck(Long, TimeUnit)} + * @since 1 by chad at 2021/9/14 + * 供{@link PulsarMQCanalConnector#getListWithoutAck(Long, TimeUnit)} * 和{@link PulsarMQCanalConnector#getFlatListWithoutAck(Long, TimeUnit)}调用 */ private List getListWithoutAck() { diff --git a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQConstants.java b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQConstants.java index 1c9c1a8e..5a6d1774 100644 --- a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQConstants.java +++ b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQConstants.java @@ -61,4 +61,9 @@ public class PulsarMQConstants { */ public static final String PULSARMQ_ADMIN_SERVER_URL = ROOT + "." + "adminServerUrl"; + /** + * Pulsar admin服务器地址 + */ + public static final String PULSARMQ_LISTENER_NAME = ROOT + "." + "listenerName"; + } diff --git a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQProducerConfig.java b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQProducerConfig.java index 1ace4aa0..40d29f3d 100644 --- a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQProducerConfig.java +++ b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/config/PulsarMQProducerConfig.java @@ -37,6 +37,11 @@ public class PulsarMQProducerConfig extends MQProperties { */ private String adminServerUrl; + /** + * listener name + */ + private String listenerName; + public String getServerUrl() { return serverUrl; } @@ -68,4 +73,12 @@ public class PulsarMQProducerConfig extends MQProperties { public void setAdminServerUrl(String adminServerUrl) { this.adminServerUrl = adminServerUrl; } + + public String getListenerName() { + return listenerName; + } + + public void setListenerName(String listenerName) { + this.listenerName = listenerName; + } } diff --git a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/consumer/CanalPulsarMQConsumer.java b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/consumer/CanalPulsarMQConsumer.java index 0a535468..ad178d9b 100644 --- a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/consumer/CanalPulsarMQConsumer.java +++ b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/consumer/CanalPulsarMQConsumer.java @@ -54,6 +54,12 @@ public class CanalPulsarMQConsumer implements CanalMsgConsumer { * 角色认证token */ private String roleToken; + + /** + * listener name + */ + private String listenerName; + /** * 订阅客户端名称 */ @@ -110,6 +116,7 @@ public class CanalPulsarMQConsumer implements CanalMsgConsumer { } this.serviceUrl = properties.getProperty(PulsarMQConstants.PULSARMQ_SERVER_URL); this.roleToken = properties.getProperty(PulsarMQConstants.PULSARMQ_ROLE_TOKEN); + this.listenerName = properties.getProperty(PulsarMQConstants.PULSARMQ_LISTENER_NAME); this.subscriptName = properties.getProperty(PulsarMQConstants.PULSARMQ_SUBSCRIPT_NAME); // 采用groupId作为subscriptName,避免所有的都是同一个订阅者名称 if (StringUtils.isEmpty(this.subscriptName)) { @@ -165,6 +172,9 @@ public class CanalPulsarMQConsumer implements CanalMsgConsumer { if (StringUtils.isNotEmpty(roleToken)) { builder.authentication(AuthenticationFactory.token(roleToken)); } + if (StringUtils.isNotEmpty(listenerName)) { + builder.authentication(AuthenticationFactory.token(listenerName)); + } pulsarClient = builder.build(); } catch (PulsarClientException e) { throw new RuntimeException(e); diff --git a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/producer/CanalPulsarMQProducer.java b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/producer/CanalPulsarMQProducer.java index b13003df..8347710a 100644 --- a/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/producer/CanalPulsarMQProducer.java +++ b/connector/pulsarmq-connector/src/main/java/com/alibaba/otter/canal/connector/pulsarmq/producer/CanalPulsarMQProducer.java @@ -73,6 +73,11 @@ public class CanalPulsarMQProducer extends AbstractMQProducer implements CanalMQ // 角色权限认证的token builder.authentication(AuthenticationFactory.token(pulsarMQProducerConfig.getRoleToken())); } + if (StringUtils.isNotEmpty(pulsarMQProducerConfig.getListenerName())) { + // listener name + builder.listenerName(pulsarMQProducerConfig.getListenerName()); + } + client = builder.build(); } catch (PulsarClientException e) { throw new RuntimeException(e); @@ -127,6 +132,11 @@ public class CanalPulsarMQProducer extends AbstractMQProducer implements CanalMQ if (!StringUtils.isEmpty(adminServerUrl)) { tmpProperties.setAdminServerUrl(adminServerUrl); } + String listenerName = PropertiesUtils.getProperty(properties, PulsarMQConstants.PULSARMQ_LISTENER_NAME); + if (!StringUtils.isEmpty(listenerName)) { + tmpProperties.setListenerName(listenerName); + } + if (logger.isDebugEnabled()) { logger.debug("Load pulsar properties ==> {}", JSON.toJSON(this.mqProperties)); } diff --git a/connector/rabbitmq-connector/src/main/java/com/alibaba/otter/canal/connector/rabbitmq/producer/CanalRabbitMQProducer.java b/connector/rabbitmq-connector/src/main/java/com/alibaba/otter/canal/connector/rabbitmq/producer/CanalRabbitMQProducer.java index 486a333c..93347877 100644 --- a/connector/rabbitmq-connector/src/main/java/com/alibaba/otter/canal/connector/rabbitmq/producer/CanalRabbitMQProducer.java +++ b/connector/rabbitmq-connector/src/main/java/com/alibaba/otter/canal/connector/rabbitmq/producer/CanalRabbitMQProducer.java @@ -82,13 +82,18 @@ public class CanalRabbitMQProducer extends AbstractMQProducer implements CanalMQ try { connect = factory.newConnection(); channel = connect.createChannel(); - channel.queueDeclare(rabbitMQProperties.getQueue(), true, false, false, null); - channel.exchangeDeclare(rabbitMQProperties - .getExchange(), rabbitMQProperties.getDeliveryMode(), true, false, false, null); - channel.queueBind(rabbitMQProperties.getQueue(), - rabbitMQProperties.getExchange(), - rabbitMQProperties.getRoutingKey()); - + String queue = rabbitMQProperties.getQueue(); + String exchange = rabbitMQProperties.getExchange(); + String deliveryMode = rabbitMQProperties.getDeliveryMode(); + String routingKey = rabbitMQProperties.getRoutingKey(); + if (!StringUtils.isEmpty(queue)) { + channel.queueDeclare(queue, true, false, false, null); + } + if (!StringUtils.isEmpty(queue) && !StringUtils.isEmpty(exchange) && !StringUtils.isEmpty(deliveryMode) + && !StringUtils.isEmpty(routingKey)) { + channel.exchangeDeclare(exchange, deliveryMode, true, false, false, null); + channel.queueBind(queue, exchange, routingKey); + } } catch (IOException | TimeoutException ex) { throw new CanalException("Start RabbitMQ producer error", ex); } diff --git a/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/JsonConversion.java b/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/JsonConversion.java index bcfd26b6..7dd92e65 100644 --- a/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/JsonConversion.java +++ b/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/JsonConversion.java @@ -1,10 +1,8 @@ package com.taobao.tddl.dbsync.binlog; -import java.nio.charset.Charset; +import static com.taobao.tddl.dbsync.binlog.event.RowsLogBuffer.*; -import static com.taobao.tddl.dbsync.binlog.event.RowsLogBuffer.appendNumber2; -import static com.taobao.tddl.dbsync.binlog.event.RowsLogBuffer.appendNumber4; -import static com.taobao.tddl.dbsync.binlog.event.RowsLogBuffer.usecondsToStr; +import java.nio.charset.Charset; /** * 处理下MySQL json二进制转化为可读的字符串 @@ -34,26 +32,25 @@ public class JsonConversion { public static final char JSONB_FALSE_LITERAL = '\2'; /* - * The size of offset or size fields in the small and the large storage - * format for JSON objects and JSON arrays. + * The size of offset or size fields in the small and the large storage format + * for JSON objects and JSON arrays. */ public static final int SMALL_OFFSET_SIZE = 2; public static final int LARGE_OFFSET_SIZE = 4; /* - * The size of key entries for objects when using the small storage format - * or the large storage format. In the small format it is 4 bytes (2 bytes - * for key length and 2 bytes for key offset). In the large format it is 6 - * (2 bytes for length, 4 bytes for offset). + * The size of key entries for objects when using the small storage format or + * the large storage format. In the small format it is 4 bytes (2 bytes for key + * length and 2 bytes for key offset). In the large format it is 6 (2 bytes for + * length, 4 bytes for offset). */ public static final int KEY_ENTRY_SIZE_SMALL = (2 + SMALL_OFFSET_SIZE); public static final int KEY_ENTRY_SIZE_LARGE = (2 + LARGE_OFFSET_SIZE); /* - * The size of value entries for objects or arrays. When using the small - * storage format, the entry size is 3 (1 byte for type, 2 bytes for - * offset). When using the large storage format, it is 5 (1 byte for type, 4 - * bytes for offset). + * The size of value entries for objects or arrays. When using the small storage + * format, the entry size is 3 (1 byte for type, 2 bytes for offset). When using + * the large storage format, it is 5 (1 byte for type, 4 bytes for offset). */ public static final int VALUE_ENTRY_SIZE_SMALL = (1 + SMALL_OFFSET_SIZE); public static final int VALUE_ENTRY_SIZE_LARGE = (1 + LARGE_OFFSET_SIZE); @@ -160,13 +157,12 @@ public class JsonConversion { if (len < n + str_len) { throw new IllegalArgumentException("illegal json data"); } - return new Json_Value(Json_enum_type.STRING, buffer.rewind() - .forward((int) n) - .getFixString((int) str_len, charset)); + return new Json_Value(Json_enum_type.STRING, + buffer.rewind().forward((int) n).getFixString((int) str_len, charset)); case JSONB_TYPE_OPAQUE: /* - * There should always be at least one byte, which tells the - * field type of the opaque value. + * There should always be at least one byte, which tells the field type of the + * opaque value. */ // The type is encoded as a uint8 that maps to an // enum_field_types. @@ -291,7 +287,7 @@ public class JsonConversion { if (i > 0) { buf.append(", "); } - buf.append('"').append(key(i, charset)).append('"'); + buf.append('"').append(escapse(key(i, charset))).append('"'); buf.append(": "); element(i, charset).toJsonString(buf, charset); } @@ -368,45 +364,45 @@ public class JsonConversion { buf.append('"').append(text).append('"'); } else if (m_field_type == LogEvent.MYSQL_TYPE_DATE || m_field_type == LogEvent.MYSQL_TYPE_DATETIME || m_field_type == LogEvent.MYSQL_TYPE_TIMESTAMP) { - long packed_value = m_data.getLong64(); - if (packed_value == 0) { - text = "0000-00-00 00:00:00"; - } else { - // 构造TimeStamp只处理到秒 - long ultime = Math.abs(packed_value); - long intpart = ultime >> 24; - int frac = (int) (ultime % (1L << 24)); - long ymd = intpart >> 17; - long ym = ymd >> 5; - long hms = intpart % (1 << 17); - // text = - // String.format("%04d-%02d-%02d %02d:%02d:%02d", - // (int) (ym / 13), - // (int) (ym % 13), - // (int) (ymd % (1 << 5)), - // (int) (hms >> 12), - // (int) ((hms >> 6) % (1 << 6)), - // (int) (hms % (1 << 6))); - StringBuilder builder = new StringBuilder(26); - appendNumber4(builder, (int) (ym / 13)); - builder.append('-'); - appendNumber2(builder, (int) (ym % 13)); - builder.append('-'); - appendNumber2(builder, (int) (ymd % (1 << 5))); - builder.append(' '); - appendNumber2(builder, (int) (hms >> 12)); - builder.append(':'); - appendNumber2(builder, (int) ((hms >> 6) % (1 << 6))); - builder.append(':'); - appendNumber2(builder, (int) (hms % (1 << 6))); - builder.append('.').append(usecondsToStr(frac, 6)); - text = builder.toString(); - } - buf.append('"').append(text).append('"'); - } else { - text = m_data.getFixString((int) m_length, charset); - buf.append('"').append(escapse(text)).append('"'); - } + long packed_value = m_data.getLong64(); + if (packed_value == 0) { + text = "0000-00-00 00:00:00"; + } else { + // 构造TimeStamp只处理到秒 + long ultime = Math.abs(packed_value); + long intpart = ultime >> 24; + int frac = (int) (ultime % (1L << 24)); + long ymd = intpart >> 17; + long ym = ymd >> 5; + long hms = intpart % (1 << 17); + // text = + // String.format("%04d-%02d-%02d %02d:%02d:%02d", + // (int) (ym / 13), + // (int) (ym % 13), + // (int) (ymd % (1 << 5)), + // (int) (hms >> 12), + // (int) ((hms >> 6) % (1 << 6)), + // (int) (hms % (1 << 6))); + StringBuilder builder = new StringBuilder(26); + appendNumber4(builder, (int) (ym / 13)); + builder.append('-'); + appendNumber2(builder, (int) (ym % 13)); + builder.append('-'); + appendNumber2(builder, (int) (ymd % (1 << 5))); + builder.append(' '); + appendNumber2(builder, (int) (hms >> 12)); + builder.append(':'); + appendNumber2(builder, (int) ((hms >> 6) % (1 << 6))); + builder.append(':'); + appendNumber2(builder, (int) (hms % (1 << 6))); + builder.append('.').append(usecondsToStr(frac, 6)); + text = builder.toString(); + } + buf.append('"').append(text).append('"'); + } else { + text = m_data.getFixString((int) m_length, charset); + buf.append('"').append(escapse(text)).append('"'); + } break; case STRING: @@ -452,7 +448,8 @@ public class JsonConversion { } public static enum Json_enum_type { - OBJECT, ARRAY, STRING, INT, UINT, DOUBLE, LITERAL_NULL, LITERAL_TRUE, LITERAL_FALSE, OPAQUE, ERROR + OBJECT, ARRAY, STRING, INT, UINT, DOUBLE, LITERAL_NULL, LITERAL_TRUE, + LITERAL_FALSE, OPAQUE, ERROR } } diff --git a/dbsync/src/test/java/com/taobao/tddl/dbsync/binlog/JsonConversion_Json_ValueTest.java b/dbsync/src/test/java/com/taobao/tddl/dbsync/binlog/JsonConversion_Json_ValueTest.java index 733d102c..3f8b8d65 100644 --- a/dbsync/src/test/java/com/taobao/tddl/dbsync/binlog/JsonConversion_Json_ValueTest.java +++ b/dbsync/src/test/java/com/taobao/tddl/dbsync/binlog/JsonConversion_Json_ValueTest.java @@ -1,5 +1,7 @@ package com.taobao.tddl.dbsync.binlog; +import java.nio.charset.Charset; + import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -48,7 +50,31 @@ public class JsonConversion_Json_ValueTest { // Act thrown.expect(IllegalArgumentException.class); JsonConversion.parse_value(type, buffer, len, charsetName); - // Method is not expected to return due to exception thrown } + + // test for 5127 + @Test + public void testJsonKeyContainsSpecialCharacter() { + + // {"internal_uri_rewrite": {"(.*)(/[^/]+\\.(mp|MP)4)$": "$1/mp4$2"}} + String jsonData = "{\"internal_uri_rewrite\": {\"(.*)(/[^/]+\\\\.(mp|MP)4)$\": \"$1/mp4$2\"}}"; + byte[] data = new byte[] { 1, 0, 74, 0, 11, 0, 20, 0, 0, 31, 0, 105, 110, 116, 101, 114, 110, 97, 108, 95, 117, + 114, 105, 95, 114, 101, 119, 114, 105, 116, 101, 1, 0, 43, 0, 11, 0, 23, 0, 12, 34, + 0, 40, 46, 42, 41, 40, 47, 91, 94, 47, 93, 43, 92, 46, 40, 109, 112, 124, 77, 80, 41, + 52, 41, 36, 8, 36, 49, 47, 109, 112, 52, 36, 50 }; + final LogBuffer buffer = new LogBuffer(data, 0, 74); + + Charset charset = Charset.forName("UTF-8"); + Json_Value jsonValue = JsonConversion.parse_value(0, buffer, 74, charset); + StringBuilder builder = new StringBuilder(); + jsonValue.toJsonString(builder, charset); + + Assert.assertEquals(builder.toString(), jsonData); + + Assert.assertEquals(jsonValue.key(0, charset), "internal_uri_rewrite"); + + Json_Value element = jsonValue.element(0, charset); + Assert.assertEquals(element.key(0, charset), "(.*)(/[^/]+\\.(mp|MP)4)$"); + } } diff --git a/deployer/src/main/resources/logback.xml b/deployer/src/main/resources/logback.xml index 682e80fa..1d30442e 100644 --- a/deployer/src/main/resources/logback.xml +++ b/deployer/src/main/resources/logback.xml @@ -23,7 +23,7 @@ 512MB - 60 + 14 @@ -50,7 +50,7 @@ 32MB - 60 + 14 @@ -71,7 +71,7 @@ 512MB - 60 + 14 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} - %msg%n diff --git a/docker/image/alidata/bin/main.sh b/docker/image/alidata/bin/main.sh index 256e7bcd..b8778757 100755 --- a/docker/image/alidata/bin/main.sh +++ b/docker/image/alidata/bin/main.sh @@ -12,8 +12,9 @@ for e in $(ls /alidata/init/*) ; do done echo "==> INIT DEFAULT" -systemctl start sshd -systemctl start crond +# disable systemctl +# systemctl start sshd +# systemctl start crond #echo "check hostname -i: `hostname -i`" #hti_num=`hostname -i|awk '{print NF}'` diff --git a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/TableMeta.java b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/TableMeta.java index 6a2b7aa8..4eb6f8ef 100644 --- a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/TableMeta.java +++ b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/TableMeta.java @@ -24,7 +24,7 @@ public class TableMeta { private String schema; private String table; private List fields = new ArrayList<>(); - private String ddl; // 表结构的DDL语句 + private String ddl; // 表结构的DDL语句 public TableMeta(){ @@ -74,6 +74,22 @@ public class TableMeta { throw new RuntimeException("unknow column : " + name); } + /** + * 尝试基于列名查找一下Filed信息,找不到时返回为null + * + * @param name + * @return + */ + public FieldMeta tryGetFieldMetaByName(String name) { + for (FieldMeta meta : fields) { + if (meta.getColumnName().equalsIgnoreCase(name)) { + return meta; + } + } + + return null; + } + public List getPrimaryFields() { List primarys = new ArrayList<>(); for (FieldMeta meta : fields) { diff --git a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/DatabaseTableMeta.java b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/DatabaseTableMeta.java index 1b0f3008..b110fe16 100644 --- a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/DatabaseTableMeta.java +++ b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/DatabaseTableMeta.java @@ -13,7 +13,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Pattern; - import com.alibaba.otter.canal.parse.driver.mysql.packets.server.FieldPacket; import com.alibaba.polardbx.druid.sql.repository.Schema; import org.apache.commons.beanutils.BeanUtils; @@ -26,6 +25,7 @@ import org.slf4j.MDC; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.alibaba.otter.canal.filter.CanalEventFilter; +import com.alibaba.otter.canal.parse.driver.mysql.packets.server.FieldPacket; import com.alibaba.otter.canal.parse.driver.mysql.packets.server.ResultSetPacket; import com.alibaba.otter.canal.parse.exception.CanalParseException; import com.alibaba.otter.canal.parse.inbound.TableMeta; @@ -247,30 +247,47 @@ public class DatabaseTableMeta implements TableMetaTSDB { } private boolean applyHistoryToDB(EntryPosition position, String schema, String ddl, String extra) { - Map content = new HashMap<>(); - content.put("destination", destination); - content.put("binlogFile", position.getJournalName()); - content.put("binlogOffest", String.valueOf(position.getPosition())); - content.put("binlogMasterId", String.valueOf(position.getServerId())); - content.put("binlogTimestamp", String.valueOf(position.getTimestamp())); - content.put("useSchema", schema); - if (content.isEmpty()) { - throw new RuntimeException("apply failed caused by content is empty in applyHistoryToDB"); - } - // 待补充 + // Map content = new HashMap<>(); + // content.put("destination", destination); + // content.put("binlogFile", position.getJournalName()); + // content.put("binlogOffest", String.valueOf(position.getPosition())); + // content.put("binlogMasterId", String.valueOf(position.getServerId())); + // content.put("binlogTimestamp", String.valueOf(position.getTimestamp())); + // content.put("useSchema", schema); + // + // if (content.isEmpty()) { + // throw new RuntimeException("apply failed caused by content is empty in + // applyHistoryToDB"); + // } + // // 待补充 + // List ddlResults = DruidDdlParser.parse(ddl, schema); + // if (ddlResults.size() > 0) { + // DdlResult ddlResult = ddlResults.get(0); + // content.put("sqlSchema", ddlResult.getSchemaName()); + // content.put("sqlTable", ddlResult.getTableName()); + // content.put("sqlType", ddlResult.getType().name()); + // content.put("sqlText", ddl); + // content.put("extra", extra); + // } + // BeanUtils.populate(metaDO, content); + + MetaHistoryDO metaDO = new MetaHistoryDO(); + metaDO.setDestination(destination); + metaDO.setBinlogFile(position.getJournalName()); + metaDO.setBinlogOffest(position.getPosition()); + metaDO.setBinlogMasterId(String.valueOf(position.getServerId())); + metaDO.setBinlogTimestamp(position.getTimestamp()); + metaDO.setUseSchema(schema); List ddlResults = DruidDdlParser.parse(ddl, schema); if (ddlResults.size() > 0) { DdlResult ddlResult = ddlResults.get(0); - content.put("sqlSchema", ddlResult.getSchemaName()); - content.put("sqlTable", ddlResult.getTableName()); - content.put("sqlType", ddlResult.getType().name()); - content.put("sqlText", ddl); - content.put("extra", extra); + metaDO.setSqlSchema(ddlResult.getSchemaName()); + metaDO.setSqlTable(ddlResult.getTableName()); + metaDO.setSqlType(ddlResult.getType().name()); + metaDO.setSqlText(ddl); + metaDO.setExtra(extra); } - - MetaHistoryDO metaDO = new MetaHistoryDO(); try { - BeanUtils.populate(metaDO, content); // 会建立唯一约束,解决: // 1. 重复的binlog file+offest // 2. 重复的masterId+timestamp @@ -328,20 +345,27 @@ public class DatabaseTableMeta implements TableMetaTSDB { } if (compareAll) { - Map content = new HashMap<>(); - content.put("destination", destination); - content.put("binlogFile", position.getJournalName()); - content.put("binlogOffest", String.valueOf(position.getPosition())); - content.put("binlogMasterId", String.valueOf(position.getServerId())); - content.put("binlogTimestamp", String.valueOf(position.getTimestamp())); - content.put("data", JSON.toJSONString(schemaDdls)); - if (content.isEmpty()) { - throw new RuntimeException("apply failed caused by content is empty in applySnapshotToDB"); - } + // Map content = new HashMap<>(); + // content.put("destination", destination); + // content.put("binlogFile", position.getJournalName()); + // content.put("binlogOffest", String.valueOf(position.getPosition())); + // content.put("binlogMasterId", String.valueOf(position.getServerId())); + // content.put("binlogTimestamp", String.valueOf(position.getTimestamp())); + // content.put("data", JSON.toJSONString(schemaDdls)); + // if (content.isEmpty()) { + // throw new RuntimeException("apply failed caused by content is empty in + // applySnapshotToDB"); + // } + // BeanUtils.populate(snapshotDO, content); MetaSnapshotDO snapshotDO = new MetaSnapshotDO(); + snapshotDO.setDestination(destination); + snapshotDO.setBinlogFile(position.getJournalName()); + snapshotDO.setBinlogOffest(position.getPosition()); + snapshotDO.setBinlogMasterId(String.valueOf(position.getServerId())); + snapshotDO.setBinlogTimestamp(position.getTimestamp()); + snapshotDO.setExtra(JSON.toJSONString(schemaDdls)); try { - BeanUtils.populate(snapshotDO, content); metaSnapshotDAO.insert(snapshotDO); } catch (Throwable e) { if (isUkDuplicateException(e)) { diff --git a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta.java b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta.java index e6b1900e..c40f4a73 100644 --- a/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta.java +++ b/parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta.java @@ -1,9 +1,6 @@ package com.alibaba.otter.canal.parse.inbound.mysql.tsdb; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import com.alibaba.polardbx.druid.DbType; @@ -38,7 +35,6 @@ import com.alibaba.polardbx.druid.util.JdbcConstants; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import com.alibaba.otter.canal.parse.inbound.TableMeta; import com.alibaba.otter.canal.parse.inbound.TableMeta.FieldMeta; import com.alibaba.otter.canal.parse.inbound.mysql.ddl.DruidDdlParser; @@ -272,11 +268,50 @@ public class MemoryTableMeta implements TableMetaTSDB { } else if (element instanceof MySqlUnique) { MySqlUnique column = (MySqlUnique) element; List uks = column.getColumns(); + // https://github.com/alibaba/canal/issues/5094 + // 处理一下函数索引 + List columnNames = new ArrayList(); for (SQLSelectOrderByItem uk : uks) { - String name = getSqlName(uk.getExpr()); - FieldMeta field = tableMeta.getFieldMetaByName(name); - field.setUnique(true); + SQLExpr sqlName = uk.getExpr(); + columnNames.addAll(getIndexColumnNames(sqlName)); } + // uniqe打标 + for (String name : columnNames) { + FieldMeta field = tableMeta.tryGetFieldMetaByName(name); + if (field != null) { + field.setUnique(true); + } + } + } + } + + private List getIndexColumnNames(SQLExpr expr) { + if (expr instanceof SQLMethodInvokeExpr) { + // 需要递归处理下函数索引, 尽可能收集一下列名 + // 常见的case: + // 1. upper(col) + // 2. left(upper(col) , 10) + // 3. col(10) + List indexExpres = ((SQLMethodInvokeExpr) expr).getArguments(); + List columnNames = new ArrayList(); + // 加上当前列 + columnNames.add(getSqlName(expr)); + // 处理函数索引列 + for (SQLExpr exArgs : indexExpres) { + if (exArgs instanceof SQLMethodInvokeExpr) { + // 加上当前列 + columnNames.add(getSqlName(exArgs)); + columnNames.addAll(getIndexColumnNames(exArgs)); + } else { + String columnName = getSqlName(exArgs); + columnNames.add(columnName); + } + } + + return columnNames; + } else { + String columnName = getSqlName(expr); + return Arrays.asList(columnName); } } diff --git a/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/FastsqlSchemaTest.java b/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/FastsqlSchemaTest.java index 38ab01ba..92fb0182 100644 --- a/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/FastsqlSchemaTest.java +++ b/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/FastsqlSchemaTest.java @@ -327,4 +327,17 @@ public class FastsqlSchemaTest { System.out.println(data.toString()); } + + @Test + public void test_function_index () throws Throwable { + SchemaRepository repository = new SchemaRepository(JdbcConstants.MYSQL); + repository.setDefaultSchema("test"); + String sql = "CREATE TABLE test1 (\n" + " id INT AUTO_INCREMENT PRIMARY KEY,\n" + + " owner_id INT NOT NULL,\n" + " code VARCHAR(100) NOT NULL,\n" + + " UNIQUE KEY uk_owner_id_upper_code (owner_id, (upper(code)))\n" + ");"; + repository.console(sql); + SchemaObject table = repository.findTable("test1"); + Assert.assertTrue(table != null); + } + } diff --git a/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta_DDL_Test.java b/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta_DDL_Test.java index 781f0f9e..34bc355b 100644 --- a/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta_DDL_Test.java +++ b/parse/src/test/java/com/alibaba/otter/canal/parse/inbound/mysql/tsdb/MemoryTableMeta_DDL_Test.java @@ -6,6 +6,9 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; +import com.alibaba.druid.sql.repository.SchemaObject; +import com.alibaba.druid.sql.repository.SchemaRepository; +import com.alibaba.druid.util.JdbcConstants; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.junit.Assert; @@ -109,4 +112,26 @@ public class MemoryTableMeta_DDL_Test { System.out.println(sourceMeta.toString()); } } + + @Test + public void test_function_index () throws Throwable { + MemoryTableMeta memoryTableMeta = new MemoryTableMeta(); + URL url = Thread.currentThread().getContextClassLoader().getResource("dummy.txt"); + File dummyFile = new File(url.getFile()); + File create = new File(dummyFile.getParent() + "/ddl", "ddl_create_function_index.sql"); + String sql = StringUtils.join(IOUtils.readLines(new FileInputStream(create)), "\n"); + memoryTableMeta.apply(null, "test", sql, null); + + List tableNames = new ArrayList<>(); + for (Schema schema : memoryTableMeta.getRepository().getSchemas()) { + tableNames.addAll(schema.showTables()); + } + + for (String table : tableNames) { + TableMeta sourceMeta = memoryTableMeta.find("test", table); + TableMeta.FieldMeta field = sourceMeta.getFieldMetaByName("code"); + System.out.println(sourceMeta.toString()); + Assert.assertTrue(field.isUnique()); + } + } } diff --git a/parse/src/test/resources/ddl/ddl_create_function_index.sql b/parse/src/test/resources/ddl/ddl_create_function_index.sql new file mode 100644 index 00000000..c26900e2 --- /dev/null +++ b/parse/src/test/resources/ddl/ddl_create_function_index.sql @@ -0,0 +1 @@ +CREATE TABLE function_index_test ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, code VARCHAR(100) NOT NULL, UNIQUE KEY uk_owner_id_upper_code (owner_id, (upper(code))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE function_index_test2 ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, code VARCHAR(100) NOT NULL, UNIQUE KEY uk_owner_id_upper_code (owner_id, ((LEFT(UPPER(code), 10)))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE function_index_test3 ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, code VARCHAR(100) NOT NULL, UNIQUE KEY uk_owner_id_upper_code (owner_id , code(10)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE function_index_test3 ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, code VARCHAR(100) NOT NULL, UNIQUE KEY uk_owner_id_upper_code (code(10)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file