diff --git a/jetlinks-components/pom.xml b/jetlinks-components/pom.xml
index 58739c25..a78e51fc 100644
--- a/jetlinks-components/pom.xml
+++ b/jetlinks-components/pom.xml
@@ -27,6 +27,7 @@
script-component
protocol-component
relation-component
+ tdengine-component
jetlinks-components
diff --git a/jetlinks-components/tdengine-component/docker-compose.yml b/jetlinks-components/tdengine-component/docker-compose.yml
new file mode 100755
index 00000000..d560b240
--- /dev/null
+++ b/jetlinks-components/tdengine-component/docker-compose.yml
@@ -0,0 +1,11 @@
+version: "2"
+services:
+ tdengine:
+ image: tdengine/tdengine:3.0.0.1
+ ports:
+ - "6030-6040:6030-6040/udp"
+ - "6030:6030"
+ - "6035:6035"
+ - "6041:6041"
+ environment:
+ TZ: CST-8
\ No newline at end of file
diff --git a/jetlinks-components/tdengine-component/pom.xml b/jetlinks-components/tdengine-component/pom.xml
new file mode 100755
index 00000000..a5407e64
--- /dev/null
+++ b/jetlinks-components/tdengine-component/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ jetlinks-components
+ org.jetlinks.community
+ 2.0.0-SNAPSHOT
+
+ 4.0.0
+
+ tdengine-component
+
+
+
+
+ org.jetlinks.community
+ timeseries-component
+ ${project.version}
+
+
+
+ io.projectreactor.netty
+ reactor-netty
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+ com.taosdata.jdbc
+ taos-jdbcdriver
+ 3.0.0
+
+
+
+ org.hswebframework
+ hsweb-easy-orm-rdb
+
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+
+ org.influxdb
+ influxdb-java
+
+
+
+ org.jetlinks.community
+ things-component
+ ${project.version}
+ true
+
+
+
+
\ No newline at end of file
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/DetectTDengineOperations.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/DetectTDengineOperations.java
new file mode 100644
index 00000000..d2afa260
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/DetectTDengineOperations.java
@@ -0,0 +1,24 @@
+package org.jetlinks.community.tdengine;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class DetectTDengineOperations implements TDengineOperations {
+ private final TDEngineDataWriter writer;
+ private final TDEngineQueryOperations query;
+
+ @Override
+ public TDEngineDataWriter forWrite() {
+ return writer;
+ }
+
+ @Override
+ public TDEngineQueryOperations forQuery() {
+ return query;
+ }
+
+ @Override
+ public void dispose() {
+ writer.dispose();
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/Point.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/Point.java
new file mode 100755
index 00000000..e8626976
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/Point.java
@@ -0,0 +1,56 @@
+package org.jetlinks.community.tdengine;
+
+import com.google.common.collect.Maps;
+import lombok.*;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class Point {
+
+ private String metric;
+
+ private String table;
+
+ private Map values = Maps.newLinkedHashMapWithExpectedSize(32);
+
+ private Map tags = Maps.newLinkedHashMapWithExpectedSize(8);
+
+ private long timestamp;
+
+ public Point(String metric, String table) {
+ this.metric = metric;
+ this.table = table;
+ }
+
+ public static Point of(String metric, String table) {
+ return new Point(metric, table);
+ }
+
+ public Point timestamp(long timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public Point tag(String metric, Object value) {
+ tags.put(metric, value);
+ return this;
+ }
+
+ public Point tags(Map values) {
+ this.tags.putAll(values);
+ return this;
+ }
+
+ public Point value(String metric, Object value) {
+ values.put(metric, value);
+ return this;
+ }
+
+ public Point values(Map values) {
+ this.values.putAll(values);
+ return this;
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineDataWriter.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineDataWriter.java
new file mode 100644
index 00000000..565c9080
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineDataWriter.java
@@ -0,0 +1,13 @@
+package org.jetlinks.community.tdengine;
+
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface TDEngineDataWriter extends Disposable {
+
+ Mono write(Point point);
+
+ Mono write(Flux points);
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineQueryOperations.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineQueryOperations.java
new file mode 100644
index 00000000..26f9cb02
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineQueryOperations.java
@@ -0,0 +1,12 @@
+package org.jetlinks.community.tdengine;
+
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+public interface TDEngineQueryOperations {
+
+ Flux query(String sql, ResultWrapper wrapper);
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineUtils.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineUtils.java
new file mode 100644
index 00000000..4f1ca573
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDEngineUtils.java
@@ -0,0 +1,49 @@
+package org.jetlinks.community.tdengine;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.reactive.function.client.ClientResponse;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class TDEngineUtils {
+
+
+ public static Mono checkExecuteResult(ClientResponse response) {
+ if (response.statusCode().isError()) {
+ return response
+ .bodyToMono(String.class)
+ .doOnNext(str -> {
+ throw new TDengineException(null, str);
+ })
+ .switchIfEmpty(Mono.error(() -> new TDengineException(null, response.statusCode().getReasonPhrase())))
+ .then(Mono.empty());
+
+ }
+ return response
+ .bodyToMono(String.class)
+ .map(json -> {
+ JSONObject obj = JSON.parseObject(json);
+ checkExecuteResult(null, obj);
+ return obj;
+ });
+
+
+ }
+
+ public static void checkExecuteResult(String sql, JSONObject result) {
+ if (result.getInteger("code") != 0) {
+ String error = result.getString("desc");
+ if (sql != null && sql.startsWith("describe") && error.contains("does not exist")) {
+ return;
+ }
+ if (sql != null) {
+ log.warn("execute tdengine sql error [{}]: [{}]", error, sql);
+ }
+
+ throw new TDengineException(sql, result.getString("desc"));
+ }
+ }
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConfiguration.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConfiguration.java
new file mode 100755
index 00000000..83530ef9
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConfiguration.java
@@ -0,0 +1,29 @@
+package org.jetlinks.community.tdengine;
+
+
+import org.jetlinks.community.tdengine.restful.RestfulTDEngineQueryOperations;
+import org.jetlinks.community.tdengine.restful.SchemalessTDEngineDataWriter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "tdengine", value = "enabled", havingValue = "true")
+@EnableConfigurationProperties(TDengineProperties.class)
+public class TDengineConfiguration {
+
+ @Bean(destroyMethod = "dispose")
+ @ConditionalOnMissingBean(TDengineOperations.class)
+ public TDengineOperations tDengineOperations(TDengineProperties properties) {
+ WebClient client = properties.getRestful().createClient();
+ SchemalessTDEngineDataWriter writer = new SchemalessTDEngineDataWriter(client,
+ properties.getDatabase(),
+ properties.getBuffer());
+
+ return new DetectTDengineOperations(writer, new RestfulTDEngineQueryOperations(client, properties.getDatabase()));
+ }
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConstants.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConstants.java
new file mode 100644
index 00000000..ad58fb81
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineConstants.java
@@ -0,0 +1,14 @@
+package org.jetlinks.community.tdengine;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+
+public interface TDengineConstants {
+
+ String COLUMN_IS_TAG = "tag";
+
+ String COLUMN_IS_TS = "ts";
+
+
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineException.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineException.java
new file mode 100755
index 00000000..faf9a455
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineException.java
@@ -0,0 +1,22 @@
+package org.jetlinks.community.tdengine;
+
+import lombok.Generated;
+import lombok.Getter;
+
+@Generated
+public class TDengineException extends RuntimeException {
+
+ @Getter
+ private final String sql;
+
+ @Generated
+ public TDengineException(String sql, String message) {
+ super(message);
+ this.sql = sql;
+ }
+ @Generated
+ public TDengineException(String sql, String message, Throwable cause) {
+ super(message, cause);
+ this.sql = sql;
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineOperations.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineOperations.java
new file mode 100644
index 00000000..f343f7b1
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineOperations.java
@@ -0,0 +1,11 @@
+package org.jetlinks.community.tdengine;
+
+import reactor.core.Disposable;
+
+public interface TDengineOperations extends Disposable {
+
+ TDEngineDataWriter forWrite();
+
+ TDEngineQueryOperations forQuery();
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineProperties.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineProperties.java
new file mode 100755
index 00000000..8e2950e2
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/TDengineProperties.java
@@ -0,0 +1,129 @@
+package org.jetlinks.community.tdengine;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.netty.channel.ChannelOption;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import io.netty.util.internal.ThreadLocalRandom;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.buffer.BufferProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.http.client.reactive.ReactorResourceFactory;
+import org.springframework.util.StringUtils;
+import org.springframework.util.unit.DataSize;
+import org.springframework.web.reactive.function.client.*;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
+import reactor.netty.resources.LoopResources;
+import reactor.netty.tcp.TcpClient;
+
+import javax.sql.DataSource;
+import javax.validation.constraints.NotBlank;
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+@Getter
+@Setter
+@ConfigurationProperties(prefix = "tdengine")
+public class TDengineProperties {
+
+ @NotBlank
+ private String database;
+
+ private Connector connector = Connector.restful;
+
+ private RestfulConnector restful = new RestfulConnector();
+
+ //缓冲配置
+ private Buffer buffer = new Buffer();
+
+ enum Connector {
+ restful
+ }
+
+ @Getter
+ @Setter
+ public static class Buffer extends BufferProperties {
+ private boolean enabled = true;
+
+ public Buffer() {
+ setFilePath("./data/tdengine-buffer");
+ setSize(3000);
+ }
+ }
+
+ @Getter
+ @Setter
+ public static class RestfulConnector {
+
+ private List endpoints = new ArrayList<>(Collections.singletonList(URI.create("http://localhost:6041/")));
+
+ private String username = "root";
+
+ private String password = "taosdata";
+
+ private int maxConnections = Runtime.getRuntime().availableProcessors() * 8;
+
+ private Duration pendingAcquireTimeout = Duration.ofSeconds(10);
+ private Duration evictInBackground = Duration.ofSeconds(60);
+
+ private Duration connectionTimeout = Duration.ofSeconds(5);
+
+ private Duration socketTimeout = Duration.ofSeconds(5);
+
+ private DataSize maxInMemorySize = DataSize.ofMegabytes(10);
+
+ public URI selectURI() {
+ // TODO: 2021/6/2 更好的负载均衡方式
+ return endpoints.get(ThreadLocalRandom.current().nextInt(endpoints.size()));
+ }
+
+ public WebClient createClient() {
+ WebClient.Builder builder = WebClient.builder();
+ URI endpoint = endpoints.get(0);
+
+ if (endpoints.size() > 1) {
+ builder = builder.filter((request, next) -> {
+ URI target = selectURI();
+ if (target.equals(endpoint)) {
+ return next.exchange(request);
+ }
+ URI uri = UriComponentsBuilder
+ .fromUri(request.url())
+ .host(target.getHost())
+ .port(target.getPort())
+ .build()
+ .toUri();
+ return next
+ .exchange(ClientRequest
+ .from(request)
+ .url(uri)
+ .build());
+ });
+ }
+
+ return builder
+ .codecs(clientCodecConfigurer -> clientCodecConfigurer
+ .defaultCodecs()
+ .maxInMemorySize((int) maxInMemorySize.toBytes()))
+ .defaultHeaders(headers -> {
+ if (StringUtils.hasText(username)) {
+ headers.setBasicAuth(username, password);
+ }
+ })
+ .baseUrl(endpoint.toString())
+ .build();
+ }
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineAlterTableSqlBuilder.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineAlterTableSqlBuilder.java
new file mode 100644
index 00000000..dfe2e224
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineAlterTableSqlBuilder.java
@@ -0,0 +1,66 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import org.hswebframework.ezorm.rdb.executor.DefaultBatchSqlRequest;
+import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.ddl.CommonAlterTableSqlBuilder;
+import org.jetlinks.community.tdengine.TDengineConstants;
+
+import static org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments.of;
+
+public class TDengineAlterTableSqlBuilder extends CommonAlterTableSqlBuilder {
+
+ @Override
+ protected void appendAddColumnCommentSql(DefaultBatchSqlRequest batch, RDBColumnMetadata column) {
+
+ }
+
+ protected PrepareSqlFragments createAlterTable(RDBColumnMetadata column) {
+ return of()
+ .addSql("ALTER", "STABLE", column.getOwner().getFullName());
+ }
+
+ @Override
+ protected void appendAddColumnSql(DefaultBatchSqlRequest batch, RDBColumnMetadata column) {
+
+ if (column.getProperty(TDengineConstants.COLUMN_IS_TS).isTrue()) {
+ return;
+ }
+ PrepareSqlFragments fragments = createAlterTable(column);
+
+ fragments
+ .addSql("ADD", column.getProperty(TDengineConstants.COLUMN_IS_TAG).isTrue() ? "COLUMN" : "TAG")
+ .addSql(column.getName())
+ .addSql(column.getDataType());
+
+ batch.addBatch(fragments.toRequest());
+ }
+
+ @Override
+ protected void appendDropColumnSql(DefaultBatchSqlRequest batch, RDBColumnMetadata drop) {
+ if (drop.getProperty(TDengineConstants.COLUMN_IS_TS).isTrue()) {
+ return;
+ }
+ PrepareSqlFragments fragments = createAlterTable(drop);
+ fragments.addSql("DROP",drop.getProperty(TDengineConstants.COLUMN_IS_TAG).isTrue() ? "COLUMN" : "TAG")
+ .addSql(drop.getName());
+
+ batch.addBatch(fragments.toRequest());
+ }
+
+ @Override
+ protected void appendAlterColumnSql(DefaultBatchSqlRequest batch,
+ RDBColumnMetadata oldColumn,
+ RDBColumnMetadata newColumn) {
+ if (newColumn.getProperty(TDengineConstants.COLUMN_IS_TS).isTrue()) {
+ return;
+ }
+
+ PrepareSqlFragments fragments = createAlterTable(newColumn);
+ fragments.addSql("MODIFY",newColumn.getProperty(TDengineConstants.COLUMN_IS_TAG).isTrue() ? "COLUMN" : "TAG")
+ .addSql(newColumn.getName())
+ .addSql(newColumn.getDataType());
+
+ batch.addBatch(fragments.toRequest());
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineCreateTableSqlBuilder.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineCreateTableSqlBuilder.java
new file mode 100644
index 00000000..1765118f
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineCreateTableSqlBuilder.java
@@ -0,0 +1,61 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.executor.SqlRequest;
+import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
+import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.ddl.CreateTableSqlBuilder;
+import org.jetlinks.community.tdengine.TDengineConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("all")
+@Getter
+@Setter
+public class TDengineCreateTableSqlBuilder implements CreateTableSqlBuilder {
+
+ @Override
+ public SqlRequest build(RDBTableMetadata table) {
+ PrepareSqlFragments sql = PrepareSqlFragments.of();
+
+ List columns = new ArrayList<>(table.getColumns().size());
+ sql.addSql("CREATE STABLE IF NOT EXISTS", table.getFullName(), "(")
+ .addSql("_ts timestamp");
+
+
+ List tags = new ArrayList<>();
+ for (RDBColumnMetadata column : table.getColumns()) {
+ if (column.getProperty(TDengineConstants.COLUMN_IS_TS).isTrue()) {
+ continue;
+ }
+ if (column.getProperty(TDengineConstants.COLUMN_IS_TAG).isTrue()) {
+ tags.add(column);
+ continue;
+ }
+ sql
+ .addSql(",")
+ .addSql(column.getQuoteName())
+ .addSql(column.getDataType());
+
+ }
+ sql.addSql(")");
+ if(!tags.isEmpty()){
+ sql.addSql("TAGS (");
+ int index= 0 ;
+ for (RDBColumnMetadata tag : tags) {
+ if(index++>0){
+ sql.addSql(",");
+ }
+ sql
+ .addSql(tag.getQuoteName())
+ .addSql(tag.getDataType());
+ }
+ sql.addSql(")");
+ }
+ return sql.toRequest();
+ }
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineDialect.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineDialect.java
new file mode 100644
index 00000000..660ab7f4
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineDialect.java
@@ -0,0 +1,52 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import org.hswebframework.ezorm.rdb.metadata.DataType;
+import org.hswebframework.ezorm.rdb.metadata.dialect.DefaultDialect;
+
+import java.math.BigDecimal;
+import java.sql.JDBCType;
+
+public class TDengineDialect extends DefaultDialect {
+
+ public TDengineDialect() {
+ super();
+ registerDataType("decimal", DataType.builder(DataType.jdbc(JDBCType.DECIMAL, BigDecimal.class),
+ column -> "DOUBLE"));
+
+ registerDataType("numeric", DataType.builder(DataType.jdbc(JDBCType.NUMERIC, BigDecimal.class),
+ column -> "DOUBLE"));
+
+ registerDataType("number", DataType.builder(DataType.jdbc(JDBCType.DOUBLE, BigDecimal.class),
+ column -> "DOUBLE"));
+
+ addDataTypeBuilder(JDBCType.VARCHAR,column->"varchar("+column.getLength()+")");
+ addDataTypeBuilder(JDBCType.TIMESTAMP,column->"timestamp");
+
+
+ }
+
+ @Override
+ public String getQuoteStart() {
+ return "`";
+ }
+
+ @Override
+ public String getQuoteEnd() {
+ return "`";
+ }
+
+ @Override
+ public boolean isColumnToUpperCase() {
+ return false;
+ }
+
+ @Override
+ public String getId() {
+ return "tdengine";
+ }
+
+ @Override
+ public String getName() {
+ return "TDengine";
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineMetadataParser.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineMetadataParser.java
new file mode 100644
index 00000000..ddf7b27c
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineMetadataParser.java
@@ -0,0 +1,90 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import lombok.AllArgsConstructor;
+import org.hswebframework.ezorm.core.meta.ObjectMetadata;
+import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers;
+import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
+import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata;
+import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
+import org.hswebframework.ezorm.rdb.metadata.parser.TableMetadataParser;
+import org.jetlinks.community.tdengine.TDengineConstants;
+import org.jetlinks.reactor.ql.utils.CastUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@AllArgsConstructor
+public class TDengineMetadataParser implements TableMetadataParser {
+
+ private final RDBSchemaMetadata schema;
+
+ private ReactiveSqlExecutor sql() {
+ return schema.findFeatureNow(ReactiveSqlExecutor.ID);
+ }
+
+ @Override
+ public List parseAllTableName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Flux parseAllTableNameReactive() {
+ return sql()
+ .select("show stables", ResultWrappers.map())
+ .mapNotNull(map -> (String) map.get("stable_name"));
+ }
+
+ @Override
+ public boolean tableExists(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Mono tableExistsReactive(String name) {
+ return parseAllTableNameReactive()
+ .hasElement(name);
+ }
+
+ @Override
+ public Optional extends ObjectMetadata> parseByName(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List extends ObjectMetadata> parseAll() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Mono parseByNameReactive(String name) {
+ RDBTableMetadata table = schema.newTable(name);
+ return sql()
+ .select("describe "+table.getFullName(), ResultWrappers.map())
+ .doOnNext(column -> table.addColumn(convertToColumn(column)))
+ .then(Mono.fromSupplier(() -> table.getColumns().isEmpty() ? null : table));
+ }
+
+ private RDBColumnMetadata convertToColumn(Map columnInfo) {
+ String note = (String) columnInfo.getOrDefault("Note", "");
+ String column = (String) columnInfo.get("Field");
+ String type = (String) columnInfo.get("Type");
+ int length = CastUtils.castNumber(columnInfo.get("Length")).intValue();
+
+ RDBColumnMetadata metadata = new RDBColumnMetadata();
+ metadata.setName(column);
+ metadata.setProperty(TDengineConstants.COLUMN_IS_TAG, "tag".equalsIgnoreCase(note));
+ metadata.setLength(length);
+ metadata.setType(schema.getDialect().convertDataType(type));
+ return metadata;
+ }
+
+ @Override
+ public Flux parseAllReactive() {
+ return parseAllTableNameReactive()
+ .flatMap(this::parseByNameReactive);
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineRestfulSqlExecutor.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineRestfulSqlExecutor.java
new file mode 100644
index 00000000..f2003a6e
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineRestfulSqlExecutor.java
@@ -0,0 +1,125 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.rdb.executor.BatchSqlRequest;
+import org.hswebframework.ezorm.rdb.executor.DefaultColumnWrapperContext;
+import org.hswebframework.ezorm.rdb.executor.SqlRequest;
+import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
+import org.jetlinks.community.tdengine.TDengineException;
+import org.jetlinks.core.utils.Reactors;
+import org.reactivestreams.Publisher;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@AllArgsConstructor
+public class TDengineRestfulSqlExecutor implements ReactiveSqlExecutor {
+
+ private final WebClient client;
+
+ @Override
+ public Mono update(Publisher request) {
+ return this
+ .doExecute(request)
+ .then(Reactors.ALWAYS_ONE);
+ }
+
+ @Override
+ public Mono execute(Publisher request) {
+ return this
+ .doExecute(request)
+ .then();
+ }
+
+ @Override
+ public Flux select(Publisher requests, ResultWrapper wrapper) {
+ return this
+ .doExecute(requests)
+ .flatMap(response -> convertQueryResult(response, wrapper));
+ }
+
+ private Flux doExecute(Publisher requests) {
+ return Flux
+ .from(requests)
+ .expand(request -> {
+ if (request instanceof BatchSqlRequest) {
+ return Flux.fromIterable(((BatchSqlRequest) request).getBatch());
+ }
+ return Flux.empty();
+ })
+ .filter(SqlRequest::isNotEmpty)
+ .concatMap(request -> {
+ String sql = request.toNativeSql();
+ log.trace("Execute ==> {}", sql);
+ return client
+ .post()
+ .uri("/rest/sql")
+ .bodyValue(sql)
+ .exchangeToMono(response -> response
+ .bodyToMono(String.class)
+ .map(json -> {
+ JSONObject result = JSON.parseObject(json);
+ checkExecuteResult(sql, result);
+ return result;
+ }));
+ });
+ }
+
+ private void checkExecuteResult(String sql, JSONObject result) {
+ if (result.getInteger("code") != 0) {
+ String error = result.getString("desc");
+ if (sql.startsWith("describe") && error.contains("does not exist")) {
+ return;
+ }
+ log.warn("execute tdengine sql error [{}]: [{}]", error, sql);
+ throw new TDengineException(sql, result.getString("desc"));
+ }
+ }
+
+ protected Flux convertQueryResult(JSONObject result, ResultWrapper wrapper) {
+
+ JSONArray head = result.getJSONArray("column_meta");
+ JSONArray data = result.getJSONArray("data");
+
+ if (CollectionUtils.isEmpty(head) || CollectionUtils.isEmpty(data)) {
+ return Flux.empty();
+ }
+ List columns = head.stream()
+ .map(v-> ((JSONArray) v).getString(0))
+ .collect(Collectors.toList());
+
+ return Flux.create(sink -> {
+ wrapper.beforeWrap(() -> columns);
+
+ for (Object rowo : data) {
+ E rowInstance = wrapper.newRowInstance();
+ JSONArray row = (JSONArray) rowo;
+ for (int i = 0; i < columns.size(); i++) {
+ String property = columns.get(i);
+ Object value = row.get(i);
+ DefaultColumnWrapperContext context = new DefaultColumnWrapperContext<>(i, property, value, rowInstance);
+ wrapper.wrapColumn(context);
+ rowInstance = context.getRowInstance();
+ }
+ if (!wrapper.completedWrapRow(rowInstance)) {
+ break;
+ }
+ if (rowInstance != null) {
+ sink.next(rowInstance);
+ }
+ }
+ wrapper.completedWrap();
+ sink.complete();
+ });
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineSchema.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineSchema.java
new file mode 100644
index 00000000..357ada04
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/metadata/TDengineSchema.java
@@ -0,0 +1,15 @@
+package org.jetlinks.community.tdengine.metadata;
+
+import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata;
+
+public class TDengineSchema extends RDBSchemaMetadata {
+
+ public TDengineSchema(String name) {
+ super(name);
+ addFeature(new TDengineMetadataParser(this));
+ addFeature(new TDengineCreateTableSqlBuilder());
+ addFeature(new TDengineAlterTableSqlBuilder());
+ }
+
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/RestfulTDEngineQueryOperations.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/RestfulTDEngineQueryOperations.java
new file mode 100644
index 00000000..7f4af0b6
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/RestfulTDEngineQueryOperations.java
@@ -0,0 +1,81 @@
+package org.jetlinks.community.tdengine.restful;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.rdb.executor.DefaultColumnWrapperContext;
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper;
+import org.jetlinks.community.tdengine.TDEngineQueryOperations;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.jetlinks.community.tdengine.TDEngineUtils.checkExecuteResult;
+
+@AllArgsConstructor
+@Slf4j
+public class RestfulTDEngineQueryOperations implements TDEngineQueryOperations {
+
+ private final WebClient client;
+
+ private final String database;
+ @Override
+ public Flux query(String sql, ResultWrapper wrapper) {
+ log.trace("Execute ==> {}", sql);
+ return client
+ .post()
+ .uri("/rest/sql/"+database)
+ .bodyValue(sql)
+ .exchangeToFlux(response -> response
+ .bodyToMono(String.class)
+ .flatMapMany(json -> {
+ JSONObject result = JSON.parseObject(json);
+ checkExecuteResult(sql, result);
+ return convertQueryResult(result, wrapper);
+ }));
+ }
+
+ protected Flux convertQueryResult(JSONObject result, ResultWrapper wrapper) {
+
+ JSONArray head = result.getJSONArray("column_meta");
+ JSONArray data = result.getJSONArray("data");
+
+ if (CollectionUtils.isEmpty(head) || CollectionUtils.isEmpty(data)) {
+ return Flux.empty();
+ }
+ List columns = head.stream()
+ .map(v -> ((JSONArray) v).getString(0))
+ .collect(Collectors.toList());
+
+ return Flux.create(sink -> {
+ wrapper.beforeWrap(() -> columns);
+
+ for (Object rowo : data) {
+ E rowInstance = wrapper.newRowInstance();
+ JSONArray row = (JSONArray) rowo;
+ for (int i = 0; i < columns.size(); i++) {
+ String property = columns.get(i);
+ Object value = row.get(i);
+ DefaultColumnWrapperContext context = new DefaultColumnWrapperContext<>(i, property, value, rowInstance);
+ wrapper.wrapColumn(context);
+ rowInstance = context.getRowInstance();
+ }
+ if (!wrapper.completedWrapRow(rowInstance)) {
+ break;
+ }
+ if (rowInstance != null) {
+ sink.next(rowInstance);
+ }
+ }
+ wrapper.completedWrap();
+ sink.complete();
+ });
+ }
+
+
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/SchemalessTDEngineDataWriter.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/SchemalessTDEngineDataWriter.java
new file mode 100644
index 00000000..060c213f
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/restful/SchemalessTDEngineDataWriter.java
@@ -0,0 +1,108 @@
+package org.jetlinks.community.tdengine.restful;
+
+import io.netty.buffer.ByteBufAllocator;
+import lombok.AllArgsConstructor;
+import org.jetlinks.community.tdengine.TDEngineDataWriter;
+import org.jetlinks.community.tdengine.TDengineProperties;
+import org.jetlinks.community.buffer.BufferSettings;
+import org.jetlinks.community.buffer.PersistenceBuffer;
+import org.jetlinks.community.tdengine.Point;
+import org.jetlinks.community.tdengine.TDEngineUtils;
+import org.jetlinks.community.utils.ErrorUtils;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.NettyDataBufferFactory;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientException;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@AllArgsConstructor
+public class SchemalessTDEngineDataWriter implements TDEngineDataWriter, Disposable {
+ private final WebClient client;
+
+ private final String database;
+
+ private final DataBufferFactory factory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
+ private final PersistenceBuffer buffer;
+
+ public SchemalessTDEngineDataWriter(WebClient client, String database, TDengineProperties.Buffer buffer) {
+ this.client = client;
+ this.database = database;
+ if (buffer.isEnabled()) {
+ this.buffer = new PersistenceBuffer(
+ BufferSettings.create("tdengine-writer.queue", buffer),
+ null,
+ list -> writeNow(list).thenReturn(false))
+ .name("tdengine")
+ .parallelism(buffer.getParallelism())
+ .retryWhenError(e -> ErrorUtils.hasException(e, WebClientException.class)
+ || ErrorUtils.hasException(e, IOException.class));
+
+ this.buffer.start();
+ } else {
+ this.buffer = null;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (null != buffer) {
+ buffer.dispose();
+ }
+ }
+
+ @Override
+ public Mono write(Point point) {
+ if (buffer == null) {
+ return writeNow(Flux.just(convertToLine(point)));
+ }
+ buffer.write(convertToLine(point));
+
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono write(Flux points) {
+ return writeNow(points.map(this::convertToLine));
+ }
+
+ private static final byte[] newLine = "\n".getBytes();
+
+ private Mono writeNow(Flux lines) {
+
+ return client
+ .post()
+ .uri(builder -> builder
+ .path("/influxdb/v1/write")
+ .queryParam("db", database)
+ .build())
+ .body(lines
+ .map(str -> {
+ byte[] data = str.getBytes();
+ return factory
+ .allocateBuffer(data.length + newLine.length)
+ .write(data)
+ .write(newLine);
+ })
+ , DataBuffer.class)
+ .exchangeToMono(TDEngineUtils::checkExecuteResult)
+ .then();
+
+
+ }
+
+ private String convertToLine(Point point) {
+ return org.influxdb.dto.Point.measurement(point.getMetric())
+ .tag((Map) point.getTags())
+ .fields(point.getValues())
+ .time(point.getTimestamp(), TimeUnit.MILLISECONDS)
+ .build()
+ .lineProtocol();
+ }
+}
diff --git a/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/term/TDengineQueryConditionBuilder.java b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/term/TDengineQueryConditionBuilder.java
new file mode 100755
index 00000000..d31f6eb9
--- /dev/null
+++ b/jetlinks-components/tdengine-component/src/main/java/org/jetlinks/community/tdengine/term/TDengineQueryConditionBuilder.java
@@ -0,0 +1,35 @@
+package org.jetlinks.community.tdengine.term;
+
+import org.hswebframework.ezorm.core.param.Term;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.AbstractTermsFragmentBuilder;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+public class TDengineQueryConditionBuilder extends AbstractTermsFragmentBuilder