From 895e013343d954c1ee4c8d08584b20ab4ffd2117 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Fri, 14 Mar 2025 19:14:54 +0800 Subject: [PATCH 01/38] fix(stream): support packaging enterprise edition. --- tools/tdgpt/script/release.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/tdgpt/script/release.sh b/tools/tdgpt/script/release.sh index 0114d60ecd..a9e47edc67 100755 --- a/tools/tdgpt/script/release.sh +++ b/tools/tdgpt/script/release.sh @@ -45,10 +45,6 @@ release_dir="${top_dir}/release" #package_name='linux' install_dir="${release_dir}/${productName}-${version}" -if [ "$pagMode" == "lite" ]; then - strip ${build_dir}/bin/${serverName} -fi - cfg_dir="${top_dir}/cfg" install_files="${script_dir}/install.sh" @@ -63,6 +59,11 @@ fi # python files mkdir -p ${install_dir}/bin && mkdir -p ${install_dir}/lib +# copy enterprise tools from outside +if [ "$edition" == "enterprise" ]; then + cp -r ${top_dir}/../../../enterprise/src/kit/tools/tdgpt/taosanalytics/misc/* ${top_dir}/taosanalytics/misc/ || : +fi + # remove cache files generated by Python interpreter TARGET_PATTERN="__pycache__" find "${top_dir}/taosanalytics/" -type d -name "$TARGET_PATTERN" -exec rm -rf {} + @@ -114,4 +115,9 @@ if [ "$exitcode" != "0" ]; then exit $exitcode fi +if [ "$edition" == "enterprise" ]; then + rm -f ${top_dir}/taosanalytics/misc/analytics* || : + echo -e "clean up" +fi + cd ${curr_dir} From c25981cb0d58349556a02f882a302497086a6895 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 09:27:04 +0800 Subject: [PATCH 02/38] feat(gpt): support lstm and do some internal refactor, add sample autoencoder model. --- .../taosanalytics/algo/ad/autoencoder.py | 16 ++-- tools/tdgpt/taosanalytics/algo/fc/lstm.py | 81 +++++++++++++++++++ .../tdgpt/taosanalytics/test/anomaly_test.py | 34 ++++---- 3 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 tools/tdgpt/taosanalytics/algo/fc/lstm.py diff --git a/tools/tdgpt/taosanalytics/algo/ad/autoencoder.py b/tools/tdgpt/taosanalytics/algo/ad/autoencoder.py index e58db3f54b..89813656fc 100644 --- a/tools/tdgpt/taosanalytics/algo/ad/autoencoder.py +++ b/tools/tdgpt/taosanalytics/algo/ad/autoencoder.py @@ -4,6 +4,7 @@ import os.path import joblib +import keras import numpy as np import pandas as pd @@ -13,8 +14,8 @@ from taosanalytics.util import create_sequences class _AutoEncoderDetectionService(AbstractAnomalyDetectionService): - name = 'ad_encoder' - desc = "anomaly detection based on auto encoder" + name = 'sample_ad_model' + desc = "sample anomaly detection model based on auto encoder" def __init__(self): super().__init__() @@ -25,7 +26,7 @@ class _AutoEncoderDetectionService(AbstractAnomalyDetectionService): self.threshold = None self.time_interval = None self.model = None - self.dir = 'ad_autoencoder' + self.dir = 'sample-ad-autoencoder' self.root_path = conf.get_model_directory() @@ -61,11 +62,6 @@ class _AutoEncoderDetectionService(AbstractAnomalyDetectionService): # Detect all the samples which are anomalies. anomalies = mae > self.threshold - # syslogger.log_inst( - # "Number of anomaly samples: %f, Indices of anomaly samples:{}". - # format(np.sum(anomalies), np.where(anomalies)) - # ) - # data i is an anomaly if samples [(i - timesteps + 1) to (i)] are anomalies ad_indices = [] for data_idx in range(self.time_interval - 1, @@ -82,13 +78,13 @@ class _AutoEncoderDetectionService(AbstractAnomalyDetectionService): name = params['model'] - module_file_path = f'{self.root_path}/{name}.dat' + module_file_path = f'{self.root_path}/{name}.keras' module_info_path = f'{self.root_path}/{name}.info' app_logger.log_inst.info("try to load module:%s", module_file_path) if os.path.exists(module_file_path): - self.model = joblib.load(module_file_path) + self.model = keras.models.load_model(module_file_path) else: app_logger.log_inst.error("failed to load autoencoder model file: %s", module_file_path) raise FileNotFoundError(f"{module_file_path} not found") diff --git a/tools/tdgpt/taosanalytics/algo/fc/lstm.py b/tools/tdgpt/taosanalytics/algo/fc/lstm.py new file mode 100644 index 0000000000..5edae7fc9f --- /dev/null +++ b/tools/tdgpt/taosanalytics/algo/fc/lstm.py @@ -0,0 +1,81 @@ +# encoding:utf-8 +# pylint: disable=c0103 +""" auto encoder algorithms to detect anomaly for time series data""" +import os.path + +import keras + +from taosanalytics.algo.forecast import insert_ts_list +from taosanalytics.conf import app_logger, conf +from taosanalytics.service import AbstractForecastService + + +class _LSTMService(AbstractForecastService): + name = 'sample_forecast_model' + desc = "sample forecast model based on LSTM" + + def __init__(self): + super().__init__() + + self.table_name = None + self.mean = None + self.std = None + self.threshold = None + self.time_interval = None + self.model = None + self.dir = 'sample-fc-lstm' + + self.root_path = conf.get_model_directory() + + self.root_path = self.root_path + f'/{self.dir}/' + + if not os.path.exists(self.root_path): + app_logger.log_inst.error( + "%s ad algorithm failed to locate default module directory:" + "%s, not active", self.__class__.__name__, self.root_path) + else: + app_logger.log_inst.info("%s ad algorithm root path is: %s", self.__class__.__name__, + self.root_path) + + def execute(self): + if self.input_is_empty(): + return [] + + if self.model is None: + raise FileNotFoundError("not load autoencoder model yet, or load model failed") + + res = self.model.predict(self.list) + + insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) + + if self.return_conf: + res1 = [res.tolist(), res.tolist(), res.tolist()], None + else: + res1 = [res.tolist()], None + + # add the conf range if required + return { + "mse": None, + "res": res1 + } + + def set_params(self, params): + + if "model" not in params: + raise ValueError("model needs to be specified") + + name = params['model'] + + module_file_path = f'{self.root_path}/{name}.keras' + # module_info_path = f'{self.root_path}/{name}.info' + + app_logger.log_inst.info("try to load module:%s", module_file_path) + + if os.path.exists(module_file_path): + self.model = keras.models.load_model(module_file_path) + else: + app_logger.log_inst.error("failed to load LSTM model file: %s", module_file_path) + raise FileNotFoundError(f"{module_file_path} not found") + + def get_params(self): + return {"dir": self.dir + '/*'} diff --git a/tools/tdgpt/taosanalytics/test/anomaly_test.py b/tools/tdgpt/taosanalytics/test/anomaly_test.py index f44a7f0d52..5333b53fa8 100644 --- a/tools/tdgpt/taosanalytics/test/anomaly_test.py +++ b/tools/tdgpt/taosanalytics/test/anomaly_test.py @@ -141,25 +141,23 @@ class AnomalyDetectionTest(unittest.TestCase): def test_autoencoder_ad(self): """for local test only, disabled it in github action""" - pass + data = self.__load_remote_data_for_ad() - # data = self.__load_remote_data_for_ad() - # - # s = loader.get_service("ad_encoder") - # s.set_input_list(data) - # - # try: - # s.set_params({"model": "ad_encoder_"}) - # except ValueError as e: - # app_logger.log_inst.error(f"failed to set the param for auto_encoder algorithm, reason:{e}") - # return - # - # r = s.execute() - # - # num_of_error = -(sum(filter(lambda x: x == -1, r))) - # self.assertEqual(num_of_error, 109) - # - # draw_ad_results(data, r, "autoencoder") + s = loader.get_service("sample_ad_model") + s.set_input_list(data) + + try: + s.set_params({"model": "sample-ad-autoencoder"}) + except ValueError as e: + app_logger.log_inst.error(f"failed to set the param for auto_encoder algorithm, reason:{e}") + return + + r = s.execute() + + num_of_error = -(sum(filter(lambda x: x == -1, r))) + draw_ad_results(data, r, "autoencoder") + + self.assertEqual(num_of_error, 109) def test_get_all_services(self): """Test get all services""" From 3ee9a476ab1c9f85158d7b4f75ec7ba589aef7df Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 09:39:38 +0800 Subject: [PATCH 03/38] feat(gpt): support lstm and do some internal refactor, add sample autoencoder model. --- .../06-TDgpt/04-forecast/04-lstm.md | 31 ++++++++++++++++++ .../04-machine-learning.md | 4 ++- .../sample-ad-autoencoder.info | Bin 0 -> 2080 bytes .../sample-ad-autoencoder.keras | Bin 0 -> 154798 bytes tools/tdgpt/taosanalytics/test/unit_test.py | 2 +- 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md create mode 100644 tools/tdgpt/model/sample-ad-autoencoder/sample-ad-autoencoder.info create mode 100644 tools/tdgpt/model/sample-ad-autoencoder/sample-ad-autoencoder.keras diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md new file mode 100644 index 0000000000..43b3fcba67 --- /dev/null +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md @@ -0,0 +1,31 @@ +--- +title: "ARIMA" +sidebar_label: "ARIMA" +--- + +本节说明 LSTM 模型的使用方法。 + +## 功能概述 + +LSTM模型即长短期记忆网络(Long Short Term Memory),是一种特殊的循环神经网络,适用于处理时间序列数据、自然语言处理等任务,通过其独特的门控机制,能够有效捕捉长期依赖关系, +解决传统RNN的梯度消失问题,从而对序列数据进行准确预测,不过它不直接提供计算的置信区间范围结果。 + + +完整的调用SQL语句如下: +```SQL +SELECT _frowts, FORECAST(i32, "algo=lstm,alpha=95,period=10,start_p=1,max_p=5,start_q=1,max_q=5") from foo +``` + +```json5 +{ +"rows": fc_rows, // 返回结果的行数 +"period": period, // 返回结果的周期性,同输入 +"alpha": alpha, // 返回结果的置信区间,同输入 +"algo": "lstm", // 返回结果使用的算法 +"mse": mse, // 拟合输入时间序列时候生成模型的最小均方误差(MSE) +"res": res // 列模式的结果 +} +``` + +### 参考文献 +- [1] Hochreiter S. Long Short-term Memory[J]. Neural Computation MIT-Press, 1997. \ No newline at end of file diff --git a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/04-machine-learning.md b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/04-machine-learning.md index b752d446eb..80a5cbe972 100644 --- a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/04-machine-learning.md +++ b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/04-machine-learning.md @@ -3,7 +3,9 @@ title: "机器学习算法" sidebar_label: "机器学习算法" --- -Autoencoder[1]: TDgpt 内置使用自编码器(Autoencoder)的异常检测算法,对周期性的时间序列数据具有较好的检测结果。使用该模型需要针对输入时序数据进行预训练,同时将训练完成的模型保存在到服务目录 `ad_autoencoder` 中,然后在 SQL 语句中指定调用该算法模型即可使用。 +Autoencoder[1]: TDgpt 内置使用自编码器(Autoencoder)的异常检测算法, +对周期性的时间序列数据具有较好的检测结果。使用该模型需要针对输入时序数据进行预训练, +同时将训练完成的模型保存在到服务目录 `ad_autoencoder` 中,然后在 SQL 语句中指定调用该算法模型即可使用。 ```SQL --- 在 options 中增加 model 的名称,ad_autoencoder_foo, 针对 foo 数据集(表)训练的采用自编码器的异常检测模型进行异常检测 diff --git a/tools/tdgpt/model/sample-ad-autoencoder/sample-ad-autoencoder.info b/tools/tdgpt/model/sample-ad-autoencoder/sample-ad-autoencoder.info new file mode 100644 index 0000000000000000000000000000000000000000..0703c99255711fea915231b648487d8d5bfc75b2 GIT binary patch literal 2080 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 TY>vG5u$Ok3D?hffyqlt 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%0Dje-jhZwq;oSK#U^9OY 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 literal 0 HcmV?d00001 diff --git a/tools/tdgpt/taosanalytics/test/unit_test.py b/tools/tdgpt/taosanalytics/test/unit_test.py index f6ecdf0d5b..aef689a8b6 100644 --- a/tools/tdgpt/taosanalytics/test/unit_test.py +++ b/tools/tdgpt/taosanalytics/test/unit_test.py @@ -99,7 +99,7 @@ class ServiceTest(unittest.TestCase): if item["type"] == "anomaly-detection": self.assertEqual(len(item["algo"]), 6) else: - self.assertEqual(len(item["algo"]), 2) + self.assertEqual(len(item["algo"]), 3) if __name__ == '__main__': From 1e225029e5a78e26acd4ba6daa1a34f9bf29279d Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 10:33:07 +0800 Subject: [PATCH 04/38] test(gpt): disable model case. --- tools/tdgpt/taosanalytics/algo/fc/arima.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tools/tdgpt/taosanalytics/algo/fc/arima.py b/tools/tdgpt/taosanalytics/algo/fc/arima.py index 9e087a5e9e..fa587c3604 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/arima.py +++ b/tools/tdgpt/taosanalytics/algo/fc/arima.py @@ -68,24 +68,6 @@ class _ArimaService(AbstractForecastService): fc = model.predict(n_periods=fc_rows, return_conf_int=self.return_conf, alpha=self.conf) - # plt.plot(source_data, label='training') - # plt.plot(xrange, actual_data, label='actual') - - # fc_list = fc.tolist() - # fc_without_diff = restore_from_diff(self.list, fc_list, 2) - # print(fc_without_diff) - - # plt.plot(xrange, fc_without_diff, label='fc') - - # residuals = pd.DataFrame(model.arima_res_.resid) - # wn = is_white_noise(residuals) - # print("residual is white noise:", wn) - - # fig, ax = plt.subplots(1, 2) - # residuals.plot(title="Residuals", ax=ax[0]) - # residuals.plot(kind='kde', title='Density', ax=ax[1]) - # plt.show() - res1 = [fc[0].tolist(), fc[1][:, 0].tolist(), fc[1][:, 1].tolist()] if self.return_conf else [fc.tolist()] From 12507c6b1816fcd38e0b31d0731fabb5ead55f21 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 10:33:50 +0800 Subject: [PATCH 05/38] test(gpt): disable model case. --- .../tdgpt/taosanalytics/test/anomaly_test.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tools/tdgpt/taosanalytics/test/anomaly_test.py b/tools/tdgpt/taosanalytics/test/anomaly_test.py index 5333b53fa8..bc173cd25b 100644 --- a/tools/tdgpt/taosanalytics/test/anomaly_test.py +++ b/tools/tdgpt/taosanalytics/test/anomaly_test.py @@ -141,23 +141,24 @@ class AnomalyDetectionTest(unittest.TestCase): def test_autoencoder_ad(self): """for local test only, disabled it in github action""" - data = self.__load_remote_data_for_ad() - - s = loader.get_service("sample_ad_model") - s.set_input_list(data) - - try: - s.set_params({"model": "sample-ad-autoencoder"}) - except ValueError as e: - app_logger.log_inst.error(f"failed to set the param for auto_encoder algorithm, reason:{e}") - return - - r = s.execute() - - num_of_error = -(sum(filter(lambda x: x == -1, r))) - draw_ad_results(data, r, "autoencoder") - - self.assertEqual(num_of_error, 109) + pass + # data = self.__load_remote_data_for_ad() + # + # s = loader.get_service("sample_ad_model") + # s.set_input_list(data) + # + # try: + # s.set_params({"model": "sample-ad-autoencoder"}) + # except ValueError as e: + # app_logger.log_inst.error(f"failed to set the param for auto_encoder algorithm, reason:{e}") + # return + # + # r = s.execute() + # + # num_of_error = -(sum(filter(lambda x: x == -1, r))) + # draw_ad_results(data, r, "autoencoder") + # + # self.assertEqual(num_of_error, 109) def test_get_all_services(self): """Test get all services""" From d88b38b65491295c45e36218aa642ef9f71b4846 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 10:45:08 +0800 Subject: [PATCH 06/38] doc: fix title error in doc. --- docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md index 43b3fcba67..dd00a18885 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md @@ -1,6 +1,6 @@ --- -title: "ARIMA" -sidebar_label: "ARIMA" +title: "LSTM" +sidebar_label: "LSTM" --- 本节说明 LSTM 模型的使用方法。 From 96b72495d3658f559b94035661c416ba9e171b5f Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Mar 2025 13:53:32 +0800 Subject: [PATCH 07/38] doc: add mlp doc. --- .../06-TDgpt/04-forecast/04-lstm.md | 2 +- .../06-TDgpt/04-forecast/05-mlp.md | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 docs/zh/06-advanced/06-TDgpt/04-forecast/05-mlp.md diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md index dd00a18885..e80bf429f5 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/04-lstm.md @@ -13,7 +13,7 @@ LSTM模型即长短期记忆网络(Long Short Term Memory),是一种特殊的 完整的调用SQL语句如下: ```SQL -SELECT _frowts, FORECAST(i32, "algo=lstm,alpha=95,period=10,start_p=1,max_p=5,start_q=1,max_q=5") from foo +SELECT _frowts, FORECAST(i32, "algo=lstm") from foo ``` ```json5 diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/05-mlp.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/05-mlp.md new file mode 100644 index 0000000000..df8b6128cd --- /dev/null +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/05-mlp.md @@ -0,0 +1,35 @@ +--- +title: "MLP" +sidebar_label: "MLP" +--- + +本节说明 MLP 模型的使用方法。 + +## 功能概述 + +MLP(MutiLayers Perceptron,多层感知机)是一种典的神经网络模型,能够通过学习历史数据的非线性关系, +捕捉时间序列中的模式并进行未来值预测。它通过多层全连接网络进行特征提取和映射, +对输入的历史数据生成预测结果。由于不直接考虑趋势或季节性变化,通常需要结合数据预处理来提升效果, +适合解决非线性和复杂的时间序列问题。 + +完整的调用SQL语句如下: + +```SQL +SELECT _frowts, FORECAST(i32, "algo=mlp") from foo +``` + +```json5 +{ +"rows": fc_rows, // 返回结果的行数 +"period": period, // 返回结果的周期性,同输入 +"alpha": alpha, // 返回结果的置信区间,同输入 +"algo": "mlp", // 返回结果使用的算法 +"mse": mse, // 拟合输入时间序列时候生成模型的最小均方误差(MSE) +"res": res // 列模式的结果 +} +``` + +### 参考文献 +- [1]Rumelhart D E, Hinton G E, Williams R J. Learning representations by back-propagating errors[J]. nature, 1986, 323(6088): 533-536. +- [2]Rosenblatt F. The perceptron: a probabilistic model for information storage and organization in the brain[J]. Psychological review, 1958, 65(6): 386. +- [3]LeCun Y, Bottou L, Bengio Y, et al. Gradient-based learning applied to document recognition[J]. Proceedings of the IEEE, 1998, 86(11): 2278-2324. \ No newline at end of file From 3e760910463660531a840c7fa4bbe624907afad6 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Wed, 19 Mar 2025 15:26:42 +0800 Subject: [PATCH 08/38] fix(gpt): add gpt --- tools/tdgpt/taosanalytics/algo/fc/gpt.py | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tools/tdgpt/taosanalytics/algo/fc/gpt.py diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py new file mode 100644 index 0000000000..a279630722 --- /dev/null +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -0,0 +1,85 @@ +# encoding:utf-8 +# pylint: disable=c0103 +""" auto encoder algorithms to detect anomaly for time series data""" +import json +import requests + +from taosanalytics.algo.forecast import insert_ts_list +from taosanalytics.conf import app_logger, conf +from taosanalytics.service import AbstractForecastService + + +class _GPTService(AbstractForecastService): + name = 'td_gpt_fc' + desc = "internal gpt forecast model based on transformer" + + def __init__(self): + super().__init__() + + self.table_name = None + self.service_host = 'http://192.168.2.90:5000/ds_predict' + self.headers = {'Content-Type': 'application/json'} + + self.std = None + self.threshold = None + self.time_interval = None + self.dir = 'internal-gpt' + + + def execute(self): + if self.list is None or len(self.list) < self.period: + raise ValueError("number of input data is less than the periods") + + if self.fc_rows <= 0: + raise ValueError("fc rows is not specified yet") + + # let's request the gpt service + data = {"input": self.list, 'next_len': self.fc_rows} + try: + response = requests.post(self.service_host, data=json.dumps(data), headers=self.headers) + except Exception as e: + app_logger.log_inst.error(f"failed to connect the service: {self.service_host} ", str(e)) + raise ValueError("error") + + # print(response) + + pred_y = response.json()['output'] + # print(f"pred_y len:{len(pred_y)}") + # print(f"pred_y:{pred_y}") + + res = { + "res": [pred_y] + } + + insert_ts_list(res["res"], self.start_ts, self.time_step, self.fc_rows) + return res + + # insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) + # + # if self.return_conf: + # res1 = [res.tolist(), res.tolist(), res.tolist()], None + # else: + # res1 = [res.tolist()], None + # + # # add the conf range if required + # return { + # "mse": None, + # "res": res1 + # } + + def set_params(self, params): + super().set_params(params) + + if "host" not in params: + raise ValueError("gpt service host needs to be specified") + + self.service_host = params['host'].trim() + + if self.service_host.startswith("https://"): + self.service_host = self.service_host.replace("https://", "http://") + elif "http://" not in self.service_host: + self.service_host = "http://" + self.service_host + + app_logger.log_inst.info("%s specify gpt host service: %s", self.__class__.__name__, + self.service_host) + From 24de2e76b55e387d5db43f5f81ded727adef74e6 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Wed, 19 Mar 2025 15:52:27 +0800 Subject: [PATCH 09/38] fix(gpt): update the test cases. --- tools/tdgpt/taosanalytics/test/unit_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tdgpt/taosanalytics/test/unit_test.py b/tools/tdgpt/taosanalytics/test/unit_test.py index aef689a8b6..5ff1717b5f 100644 --- a/tools/tdgpt/taosanalytics/test/unit_test.py +++ b/tools/tdgpt/taosanalytics/test/unit_test.py @@ -99,7 +99,7 @@ class ServiceTest(unittest.TestCase): if item["type"] == "anomaly-detection": self.assertEqual(len(item["algo"]), 6) else: - self.assertEqual(len(item["algo"]), 3) + self.assertEqual(len(item["algo"]), 4) if __name__ == '__main__': From fa2803121ad15d399970407d2b232057a9006c1a Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Wed, 19 Mar 2025 17:31:56 +0800 Subject: [PATCH 10/38] fix(gpt): update the test cases. --- tools/tdgpt/taosanalytics/algo/fc/gpt.py | 22 +++++-------------- .../tdgpt/taosanalytics/test/forecast_test.py | 20 +++++++++++++++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py index a279630722..898591cce4 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/gpt.py +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -39,13 +39,13 @@ class _GPTService(AbstractForecastService): response = requests.post(self.service_host, data=json.dumps(data), headers=self.headers) except Exception as e: app_logger.log_inst.error(f"failed to connect the service: {self.service_host} ", str(e)) - raise ValueError("error") + raise e - # print(response) + if response.status_code != 200: + app_logger.log_inst.error(f"failed to connect the service: {self.service_host} ") + raise ValueError("invalid host url") pred_y = response.json()['output'] - # print(f"pred_y len:{len(pred_y)}") - # print(f"pred_y:{pred_y}") res = { "res": [pred_y] @@ -54,18 +54,6 @@ class _GPTService(AbstractForecastService): insert_ts_list(res["res"], self.start_ts, self.time_step, self.fc_rows) return res - # insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) - # - # if self.return_conf: - # res1 = [res.tolist(), res.tolist(), res.tolist()], None - # else: - # res1 = [res.tolist()], None - # - # # add the conf range if required - # return { - # "mse": None, - # "res": res1 - # } def set_params(self, params): super().set_params(params) @@ -73,7 +61,7 @@ class _GPTService(AbstractForecastService): if "host" not in params: raise ValueError("gpt service host needs to be specified") - self.service_host = params['host'].trim() + self.service_host = params['host'] if self.service_host.startswith("https://"): self.service_host = self.service_host.replace("https://", "http://") diff --git a/tools/tdgpt/taosanalytics/test/forecast_test.py b/tools/tdgpt/taosanalytics/test/forecast_test.py index 1e4874b8c8..6951c700b6 100644 --- a/tools/tdgpt/taosanalytics/test/forecast_test.py +++ b/tools/tdgpt/taosanalytics/test/forecast_test.py @@ -8,7 +8,7 @@ import pandas as pd sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../") from taosanalytics.algo.forecast import draw_fc_results -from taosanalytics.conf import setup_log_info +from taosanalytics.conf import setup_log_info, app_logger from taosanalytics.servicemgmt import loader @@ -30,7 +30,8 @@ class ForecastTest(unittest.TestCase): ts_list = data[['Passengers']].index.tolist() dst_list = [int(item.timestamp()) for item in ts_list] - return data[['Passengers']].values.tolist(), dst_list + return data['Passengers'].values.tolist(), dst_list + def test_holt_winters_forecast(self): """ test holt winters forecast with invalid and then valid parameters""" @@ -111,5 +112,20 @@ class ForecastTest(unittest.TestCase): draw_fc_results(data, len(r["res"]) > 1, r["res"], rows, "arima") + def test_gpt_fc(self): + """for local test only, disabled it in github action""" + data, ts = self.get_input_list() + pass + + s = loader.get_service("td_gpt_fc") + s.set_input_list(data, ts) + + s.set_params({"host":'192.168.2.90:5000/ds_predict', 'fc_rows': 10, 'start_ts': 171000000, 'time_step': 86400*30}) + r = s.execute() + + rows = len(r["res"][0]) + draw_fc_results(data, False, r["res"], rows, "gpt") + + if __name__ == '__main__': unittest.main() From 79876a1f2f6243013fc1a2eca5fce0f65192db15 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Wed, 19 Mar 2025 17:50:19 +0800 Subject: [PATCH 11/38] test(gpt): disable gpt test in github action. --- tools/tdgpt/taosanalytics/test/forecast_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/tdgpt/taosanalytics/test/forecast_test.py b/tools/tdgpt/taosanalytics/test/forecast_test.py index 6951c700b6..4b2368c6ba 100644 --- a/tools/tdgpt/taosanalytics/test/forecast_test.py +++ b/tools/tdgpt/taosanalytics/test/forecast_test.py @@ -117,14 +117,14 @@ class ForecastTest(unittest.TestCase): data, ts = self.get_input_list() pass - s = loader.get_service("td_gpt_fc") - s.set_input_list(data, ts) - - s.set_params({"host":'192.168.2.90:5000/ds_predict', 'fc_rows': 10, 'start_ts': 171000000, 'time_step': 86400*30}) - r = s.execute() - - rows = len(r["res"][0]) - draw_fc_results(data, False, r["res"], rows, "gpt") + # s = loader.get_service("td_gpt_fc") + # s.set_input_list(data, ts) + # + # s.set_params({"host":'192.168.2.90:5000/ds_predict', 'fc_rows': 10, 'start_ts': 171000000, 'time_step': 86400*30}) + # r = s.execute() + # + # rows = len(r["res"][0]) + # draw_fc_results(data, False, r["res"], rows, "gpt") if __name__ == '__main__': From 32d48a8e85de03e24a9420a8c8332533057a9ee0 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Wed, 19 Mar 2025 18:20:28 +0800 Subject: [PATCH 12/38] fix(gpt): check the grant, check the rsp code. --- source/dnode/mnode/impl/src/mndAnode.c | 5 ++ tools/tdgpt/taosanalytics/algo/fc/gpt.py | 5 +- .../taosanalytics/test/restful_api_test.py | 51 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/source/dnode/mnode/impl/src/mndAnode.c b/source/dnode/mnode/impl/src/mndAnode.c index bd0a4f3138..0777c2e247 100644 --- a/source/dnode/mnode/impl/src/mndAnode.c +++ b/source/dnode/mnode/impl/src/mndAnode.c @@ -371,6 +371,11 @@ static int32_t mndProcessCreateAnodeReq(SRpcMsg *pReq) { SAnodeObj *pObj = NULL; SMCreateAnodeReq createReq = {0}; + if ((code = grantCheck(TSDB_GRANT_TD_GPT)) != TSDB_CODE_SUCCESS) { + mError("failed to create anode, code:%s", tstrerror(code)); + goto _OVER; + } + TAOS_CHECK_GOTO(tDeserializeSMCreateAnodeReq(pReq->pCont, pReq->contLen, &createReq), NULL, _OVER); mInfo("anode:%s, start to create", createReq.url); diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py index 898591cce4..362de3fe03 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/gpt.py +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -41,9 +41,12 @@ class _GPTService(AbstractForecastService): app_logger.log_inst.error(f"failed to connect the service: {self.service_host} ", str(e)) raise e - if response.status_code != 200: + if response.status_code == 404: app_logger.log_inst.error(f"failed to connect the service: {self.service_host} ") raise ValueError("invalid host url") + elif response.status_code != 200: + app_logger.log_inst.error(f"failed to request the service: {self.service_host}, reason: {response.text}") + raise ValueError(f"failed to request the service, {response.text}") pred_y = response.json()['output'] diff --git a/tools/tdgpt/taosanalytics/test/restful_api_test.py b/tools/tdgpt/taosanalytics/test/restful_api_test.py index 6463343e00..7dc43ab890 100644 --- a/tools/tdgpt/taosanalytics/test/restful_api_test.py +++ b/tools/tdgpt/taosanalytics/test/restful_api_test.py @@ -257,3 +257,54 @@ class RestfulTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json["rows"], -1) + + + def test_gpt_restful_service(self): + response = self.client.post('/forecast', json={ + "schema": [ + ["ts", "TIMESTAMP", 8], + ["val", "INT", 4] + ], + "data": [ + [ + 1577808000000, 1577808001000, 1577808002000, 1577808003000, 1577808004000, + 1577808005000, 1577808006000, 1577808007000, 1577808008000, 1577808009000, + 1577808010000, 1577808011000, 1577808012000, 1577808013000, 1577808014000, + 1577808015000, 1577808016000, 1577808017000, 1577808018000, 1577808019000, + 1577808020000, 1577808021000, 1577808022000, 1577808023000, 1577808024000, + 1577808025000, 1577808026000, 1577808027000, 1577808028000, 1577808029000, + 1577808030000, 1577808031000, 1577808032000, 1577808033000, 1577808034000, + 1577808035000, 1577808036000, 1577808037000, 1577808038000, 1577808039000, + 1577808040000, 1577808041000, 1577808042000, 1577808043000, 1577808044000, + 1577808045000, 1577808046000, 1577808047000, 1577808048000, 1577808049000, + 1577808050000, 1577808051000, 1577808052000, 1577808053000, 1577808054000, + 1577808055000, 1577808056000, 1577808057000, 1577808058000, 1577808059000, + 1577808060000, 1577808061000, 1577808062000, 1577808063000, 1577808064000, + 1577808065000, 1577808066000, 1577808067000, 1577808068000, 1577808069000, + 1577808070000, 1577808071000, 1577808072000, 1577808073000, 1577808074000, + 1577808075000, 1577808076000, 1577808077000, 1577808078000, 1577808079000, + 1577808080000, 1577808081000, 1577808082000, 1577808083000, 1577808084000, + 1577808085000, 1577808086000, 1577808087000, 1577808088000, 1577808089000, + 1577808090000, 1577808091000, 1577808092000, 1577808093000, 1577808094000, + 1577808095000 + ], + [ + 13, 14, 8, 10, 16, 26, 32, 27, 18, 32, 36, 24, 22, 23, 22, 18, 25, 21, 21, + 14, 8, 11, 14, 23, 18, 17, 19, 20, 22, 19, 13, 26, 13, 14, 22, 24, 21, 22, + 26, 21, 23, 24, 27, 41, 31, 27, 35, 26, 28, 36, 39, 21, 17, 22, 17, 19, 15, + 34, 10, 15, 22, 18, 15, 20, 15, 22, 19, 16, 30, 27, 29, 23, 20, 16, 21, 21, + 25, 16, 18, 15, 18, 14, 10, 15, 8, 15, 6, 11, 8, 7, 13, 10, 23, 16, 15, 25 + ] + ], + "option": "algo=td_gpt_fc", + "algo": "td_gpt_fc", + "prec": "ms", + "wncheck": 0, + "return_conf": 0, + "forecast_rows": 10, + "conf": 95, + "start": 1577808096000, + "every": 1000, + "rows": 21, + "protocol": 1.0 + }) From f42ce412ae4f8ee0b2d1c4eed3e2df2f186a2de3 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Thu, 20 Mar 2025 11:23:19 +0800 Subject: [PATCH 13/38] fix(gpt): limit the forecast rows. --- include/common/tanalytics.h | 5 +++-- source/common/src/tanalytics.c | 4 ++-- source/libs/executor/src/anomalywindowoperator.c | 2 +- source/libs/executor/src/forecastoperator.c | 4 ++-- tools/tdgpt/taosanalytics/algo/fc/arima.py | 3 +++ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/common/tanalytics.h b/include/common/tanalytics.h index 0fb1d543f7..976e89beb3 100644 --- a/include/common/tanalytics.h +++ b/include/common/tanalytics.h @@ -29,9 +29,10 @@ extern "C" { #define ANALY_FORECAST_DEFAULT_CONF 95 #define ANALY_FORECAST_DEFAULT_WNCHECK 1 #define ANALY_FORECAST_MAX_ROWS 40000 +#define ANALY_FORECAST_RES_MAX_ROWS 1024 #define ANALY_ANOMALY_WINDOW_MAX_ROWS 40000 -#define ANALY_DEFAULT_TIMEOUT 60 -#define ANALY_MAX_TIMEOUT 600 +#define ANALY_DEFAULT_TIMEOUT 60 +#define ANALY_MAX_TIMEOUT 600 typedef struct { EAnalAlgoType type; diff --git a/source/common/src/tanalytics.c b/source/common/src/tanalytics.c index 8818839f09..4dd329fb4c 100644 --- a/source/common/src/tanalytics.c +++ b/source/common/src/tanalytics.c @@ -434,7 +434,7 @@ static int32_t taosAnalyJsonBufWriteStr(SAnalyticBuf *pBuf, const char *buf, int static int32_t taosAnalyJsonBufWriteStart(SAnalyticBuf *pBuf) { return taosAnalyJsonBufWriteStr(pBuf, "{\n", 0); } -static int32_t tsosAnalJsonBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols) { +static int32_t tsosAnalyJsonBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols) { pBuf->filePtr = taosOpenFile(pBuf->fileName, TD_FILE_CREATE | TD_FILE_WRITE | TD_FILE_TRUNC | TD_FILE_WRITE_THROUGH); if (pBuf->filePtr == NULL) { return terrno; @@ -666,7 +666,7 @@ void taosAnalyBufDestroy(SAnalyticBuf *pBuf) { int32_t tsosAnalyBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols) { if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) { - return tsosAnalJsonBufOpen(pBuf, numOfCols); + return tsosAnalyJsonBufOpen(pBuf, numOfCols); } else { return TSDB_CODE_ANA_BUF_INVALID_TYPE; } diff --git a/source/libs/executor/src/anomalywindowoperator.c b/source/libs/executor/src/anomalywindowoperator.c index 379177bb06..10643c3a0d 100644 --- a/source/libs/executor/src/anomalywindowoperator.c +++ b/source/libs/executor/src/anomalywindowoperator.c @@ -382,7 +382,7 @@ static int32_t anomalyAnalysisWindow(SOperatorInfo* pOperator) { SAnalyticBuf analyBuf = {.bufType = ANALYTICS_BUF_TYPE_JSON}; char dataBuf[64] = {0}; int32_t code = 0; - int64_t ts = 0; + int64_t ts = taosGetTimestampMs(); int32_t lino = 0; const char* pId = GET_TASKID(pOperator->pTaskInfo); diff --git a/source/libs/executor/src/forecastoperator.c b/source/libs/executor/src/forecastoperator.c index e9185824a3..9139241b04 100644 --- a/source/libs/executor/src/forecastoperator.c +++ b/source/libs/executor/src/forecastoperator.c @@ -158,8 +158,8 @@ static int32_t forecastCloseBuf(SForecastSupp* pSupp, const char* id) { qDebug("%s forecast rows not found from %s, use default:%" PRId64, id, pSupp->algoOpt, pSupp->optRows); } - if (pSupp->optRows > ANALY_FORECAST_MAX_ROWS) { - qError("%s required too many forecast rows, max allowed:%d, required:%" PRId64, id, ANALY_FORECAST_MAX_ROWS, + if (pSupp->optRows > ANALY_FORECAST_RES_MAX_ROWS) { + qError("%s required too many forecast rows, max allowed:%d, required:%" PRId64, id, ANALY_FORECAST_RES_MAX_ROWS, pSupp->optRows); return TSDB_CODE_ANA_ANODE_TOO_MANY_ROWS; } diff --git a/tools/tdgpt/taosanalytics/algo/fc/arima.py b/tools/tdgpt/taosanalytics/algo/fc/arima.py index fa587c3604..787cb757df 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/arima.py +++ b/tools/tdgpt/taosanalytics/algo/fc/arima.py @@ -83,6 +83,9 @@ class _ArimaService(AbstractForecastService): if self.list is None or len(self.list) < self.period: raise ValueError("number of input data is less than the periods") + if len(self.list) > 3000: + raise ValueError("number of input data is too large") + if self.fc_rows <= 0: raise ValueError("fc rows is not specified yet") From 383b985869cc853530954ed5a7845a60b6077919 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Thu, 20 Mar 2025 11:48:29 +0800 Subject: [PATCH 14/38] fix(gpt): use default gpt service --- tools/tdgpt/taosanalytics/algo/fc/gpt.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py index 362de3fe03..8a65f88cc3 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/gpt.py +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -17,7 +17,7 @@ class _GPTService(AbstractForecastService): super().__init__() self.table_name = None - self.service_host = 'http://192.168.2.90:5000/ds_predict' + self.service_host = 'http://127.0.0.1:5000/ds_predict' self.headers = {'Content-Type': 'application/json'} self.std = None @@ -61,15 +61,13 @@ class _GPTService(AbstractForecastService): def set_params(self, params): super().set_params(params) - if "host" not in params: - raise ValueError("gpt service host needs to be specified") + if "host" in params: + self.service_host = params['host'] - self.service_host = params['host'] - - if self.service_host.startswith("https://"): - self.service_host = self.service_host.replace("https://", "http://") - elif "http://" not in self.service_host: - self.service_host = "http://" + self.service_host + if self.service_host.startswith("https://"): + self.service_host = self.service_host.replace("https://", "http://") + elif "http://" not in self.service_host: + self.service_host = "http://" + self.service_host app_logger.log_inst.info("%s specify gpt host service: %s", self.__class__.__name__, self.service_host) From c52df0bb24f158e0d2df9fd6adb3733135231eae Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Thu, 20 Mar 2025 11:54:43 +0800 Subject: [PATCH 15/38] fix(gpt): update error code. --- include/util/taoserror.h | 1 + source/libs/executor/src/anomalywindowoperator.c | 2 +- source/libs/executor/src/forecastoperator.c | 2 +- source/util/src/terror.c | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/util/taoserror.h b/include/util/taoserror.h index 7f49dfb633..ee523166a6 100644 --- a/include/util/taoserror.h +++ b/include/util/taoserror.h @@ -514,6 +514,7 @@ int32_t taosGetErrSize(); #define TSDB_CODE_ANA_ANODE_RETURN_ERROR TAOS_DEF_ERROR_CODE(0, 0x0445) #define TSDB_CODE_ANA_ANODE_TOO_MANY_ROWS TAOS_DEF_ERROR_CODE(0, 0x0446) #define TSDB_CODE_ANA_WN_DATA TAOS_DEF_ERROR_CODE(0, 0x0447) +#define TSDB_CODE_ANA_INTERNAL_ERROR TAOS_DEF_ERROR_CODE(0, 0x0448) // mnode-sma #define TSDB_CODE_MND_SMA_ALREADY_EXIST TAOS_DEF_ERROR_CODE(0, 0x0480) diff --git a/source/libs/executor/src/anomalywindowoperator.c b/source/libs/executor/src/anomalywindowoperator.c index 10643c3a0d..d46c429bef 100644 --- a/source/libs/executor/src/anomalywindowoperator.c +++ b/source/libs/executor/src/anomalywindowoperator.c @@ -327,7 +327,7 @@ static int32_t anomalyParseJson(SJson* pJson, SArray* pWindows, const char* pId) qError("%s failed to exec forecast, msg:%s", pId, pMsg); } - return TSDB_CODE_ANA_WN_DATA; + return TSDB_CODE_ANA_INTERNAL_ERROR; } else if (rows == 0) { return TSDB_CODE_SUCCESS; } diff --git a/source/libs/executor/src/forecastoperator.c b/source/libs/executor/src/forecastoperator.c index 9139241b04..b0966a65fe 100644 --- a/source/libs/executor/src/forecastoperator.c +++ b/source/libs/executor/src/forecastoperator.c @@ -235,7 +235,7 @@ static int32_t forecastAnalysis(SForecastSupp* pSupp, SSDataBlock* pBlock, const } tjsonDelete(pJson); - return TSDB_CODE_ANA_WN_DATA; + return TSDB_CODE_ANA_INTERNAL_ERROR; } if (code < 0) { diff --git a/source/util/src/terror.c b/source/util/src/terror.c index eb80708b86..3b6766fc27 100644 --- a/source/util/src/terror.c +++ b/source/util/src/terror.c @@ -377,6 +377,7 @@ TAOS_DEFINE_ERROR(TSDB_CODE_ANA_BUF_INVALID_TYPE, "Analysis invalid buffe TAOS_DEFINE_ERROR(TSDB_CODE_ANA_ANODE_RETURN_ERROR, "Analysis failed since anode return error") TAOS_DEFINE_ERROR(TSDB_CODE_ANA_ANODE_TOO_MANY_ROWS, "Analysis failed since too many input rows for anode") TAOS_DEFINE_ERROR(TSDB_CODE_ANA_WN_DATA, "white-noise data not processed") +TAOS_DEFINE_ERROR(TSDB_CODE_ANA_INTERNAL_ERROR, "tdgpt internal error, not processed") // mnode-sma TAOS_DEFINE_ERROR(TSDB_CODE_MND_SMA_ALREADY_EXIST, "SMA already exists") From c4004719172ba5e9d2154a773ba0503cbb29355c Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Thu, 20 Mar 2025 23:25:02 +0800 Subject: [PATCH 16/38] fix(gpt): update the version. --- tools/tdgpt/taosanalytics/app.py | 3 ++- tools/tdgpt/taosanalytics/test/restful_api_test.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/tdgpt/taosanalytics/app.py b/tools/tdgpt/taosanalytics/app.py index 7be41489e7..b67fa5f95d 100644 --- a/tools/tdgpt/taosanalytics/app.py +++ b/tools/tdgpt/taosanalytics/app.py @@ -22,11 +22,12 @@ app_logger.set_handler(conf.get_log_path()) app_logger.set_log_level(conf.get_log_level()) loader.load_all_service() +_ANODE_VER = 'TDgpt - TDengine© Time-Series Data Analytics Platform (ver 3.3.6.0)' @app.route("/") def start(): """ default rsp """ - return "TDengine© Time Series Data Analytics Platform (ver 1.0.1)" + return _ANODE_VER @app.route("/status") diff --git a/tools/tdgpt/taosanalytics/test/restful_api_test.py b/tools/tdgpt/taosanalytics/test/restful_api_test.py index 7dc43ab890..818841dbe1 100644 --- a/tools/tdgpt/taosanalytics/test/restful_api_test.py +++ b/tools/tdgpt/taosanalytics/test/restful_api_test.py @@ -23,7 +23,8 @@ class RestfulTest(TestCase): """ test asscess default main page """ response = self.client.get('/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_length, len("TDengine© Time Series Data Analytics Platform (ver 1.0.1)") + 1) + self.assertEqual(response.content_length, + len("TDgpt - TDengine© Time-Series Data Analytics Platform (ver 3.3.6.0)") + 1) def test_load_status(self): """ test load the server status """ From 8cddd7ebfe36bb55b4e4ee34ec9224d22bb3b591 Mon Sep 17 00:00:00 2001 From: WANG MINGMING Date: Fri, 21 Mar 2025 10:46:20 +0800 Subject: [PATCH 17/38] fix(sml): process space in the end if writing raw data in sml & change some log level #30306 fix(sml): process space in the end if writing raw data in sml & change some log level --- source/client/src/clientSmlTelnet.c | 2 +- source/util/src/tmempool.c | 2 +- source/util/test/memPoolTest.cpp | 2 +- utils/test/c/sml_test.c | 28 ++++++++++++++++++++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/source/client/src/clientSmlTelnet.c b/source/client/src/clientSmlTelnet.c index dd264da11e..0427196b83 100644 --- a/source/client/src/clientSmlTelnet.c +++ b/source/client/src/clientSmlTelnet.c @@ -75,7 +75,7 @@ static int32_t smlProcessTagTelnet(SSmlHandle *info, char *data, char *sqlEnd){ const char *sql = data; while (sql < sqlEnd) { JUMP_SPACE(sql, sqlEnd) - if (unlikely(*sql == '\0')) break; + if (unlikely(*sql == '\0' || *sql == '\n')) break; const char *key = sql; size_t keyLen = 0; diff --git a/source/util/src/tmempool.c b/source/util/src/tmempool.c index 0a56c69505..96a9cfcea2 100644 --- a/source/util/src/tmempool.c +++ b/source/util/src/tmempool.c @@ -1029,7 +1029,7 @@ void mpUpdateSystemAvailableMemorySize() { atomic_store_64(&tsCurrentAvailMemorySize, sysAvailSize); - uDebug("system available memory size: %" PRId64, sysAvailSize); + uTrace("system available memory size: %" PRId64, sysAvailSize); } void mpSchedTrim(int64_t* loopTimes) { diff --git a/source/util/test/memPoolTest.cpp b/source/util/test/memPoolTest.cpp index 8af8b01368..a0086b8d1c 100644 --- a/source/util/test/memPoolTest.cpp +++ b/source/util/test/memPoolTest.cpp @@ -729,7 +729,7 @@ int32_t mptGetMemPoolMaxMemSize(void* pHandle, int64_t* maxSize) { } if (TSDB_CODE_SUCCESS != code) { - uError("get system avaiable memory size failed, error: 0x%x", code); + uError("get system available memory size failed, error: 0x%x", code); return code; } diff --git a/utils/test/c/sml_test.c b/utils/test/c/sml_test.c index 0907c2a641..4db5a1908d 100644 --- a/utils/test/c/sml_test.c +++ b/utils/test/c/sml_test.c @@ -87,7 +87,8 @@ int smlProcess_telnet_Test() { const char *sql1[] = {"sys.if.bytes.out 1479496100 1.3E0 host=web01 interface=eth0", "sys.if.bytes.out 1479496101 1.3E1 interface=eth0 host=web01 ", "sys.if.bytes.out 1479496102 1.3E3 network=tcp", - " sys.procs.running 1479496100 42 host=web01 "}; + " sys.procs.running 1479496100 42 host=web01 ", + " newline 1479496100 42 host=web\n01 t=fsb\n "}; // for(int i = 0; i < 4; i++){ // strncpy(sql[i], sql1[i], 128); @@ -2355,12 +2356,35 @@ int sml_td17324_Test() { return code; } +int smlProcess_34114_Test() { + TAOS *taos = taos_connect("localhost", "root", "taosdata", NULL, 0); + + TAOS_RES *pRes = taos_query(taos, "create database if not exists sml_34114_db schemaless 1"); + taos_free_result(pRes); + + pRes = taos_query(taos, "use sml_34114_db"); + taos_free_result(pRes); + + char *sql = {"sys.if.bytes.out 1479496100 1.3E0 host=web01 interface=eth0 \nsys.if.bytes.out 1479496101 1.3E1 interface=eth0 host=web01 "}; + int32_t totalRows = 0; + pRes = taos_schemaless_insert_raw(taos, sql, strlen(sql), &totalRows, TSDB_SML_TELNET_PROTOCOL, + TSDB_SML_TIMESTAMP_NANO_SECONDS); + printf("%s result:%s\n", __FUNCTION__, taos_errstr(pRes)); + int code = taos_errno(pRes); + taos_free_result(pRes); + taos_close(taos); + + return code; +} + int main(int argc, char *argv[]) { if (argc == 2) { taos_options(TSDB_OPTION_CONFIGDIR, argv[1]); } - int ret = smlProcess_json0_Test(); + int ret = smlProcess_34114_Test(); + ASSERT(!ret); + ret = smlProcess_json0_Test(); ASSERT(!ret); ret = sml_ts5528_test(); ASSERT(!ret); From 47004861c70be0ce31c96fba6765211bf5030086 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Fri, 21 Mar 2025 12:52:41 +0800 Subject: [PATCH 18/38] fix(gpt): 1. fix error in anomalywindow count; 2. update gpt model name; 3. update the expired test case. 4. support the user specified valid_code --- source/libs/executor/src/anomalywindowoperator.c | 2 +- tests/script/tsim/analytics/basic0.sim | 4 ++-- tools/tdgpt/cfg/taosanode.ini | 2 +- tools/tdgpt/taosanalytics/algo/anomaly.py | 16 +++++++++------- tools/tdgpt/taosanalytics/algo/fc/gpt.py | 3 +-- tools/tdgpt/taosanalytics/app.py | 4 +--- tools/tdgpt/taosanalytics/service.py | 7 +++++++ tools/tdgpt/taosanalytics/test/anomaly_test.py | 10 +++++----- tools/tdgpt/taosanalytics/util.py | 4 ++-- 9 files changed, 29 insertions(+), 23 deletions(-) diff --git a/source/libs/executor/src/anomalywindowoperator.c b/source/libs/executor/src/anomalywindowoperator.c index d46c429bef..425809e1a2 100644 --- a/source/libs/executor/src/anomalywindowoperator.c +++ b/source/libs/executor/src/anomalywindowoperator.c @@ -593,7 +593,7 @@ static int32_t anomalyAggregateBlocks(SOperatorInfo* pOperator) { for (int32_t r = 0; r < pBlock->info.rows; ++r) { TSKEY key = tsList[r]; - bool keyInWin = (key >= pSupp->curWin.skey && key < pSupp->curWin.ekey); + bool keyInWin = (key >= pSupp->curWin.skey && key <= pSupp->curWin.ekey); bool lastRow = (r == pBlock->info.rows - 1); if (keyInWin) { diff --git a/tests/script/tsim/analytics/basic0.sim b/tests/script/tsim/analytics/basic0.sim index 0d9a29a19b..c536b2680f 100644 --- a/tests/script/tsim/analytics/basic0.sim +++ b/tests/script/tsim/analytics/basic0.sim @@ -23,8 +23,8 @@ endi print =============== show info sql show anodes full -if $rows != 8 then - print expect 8 , actual $rows +if $rows != 10 then + print expect 10 , actual $rows return -1 endi diff --git a/tools/tdgpt/cfg/taosanode.ini b/tools/tdgpt/cfg/taosanode.ini index 12ba6b5776..2107710a30 100755 --- a/tools/tdgpt/cfg/taosanode.ini +++ b/tools/tdgpt/cfg/taosanode.ini @@ -78,4 +78,4 @@ model-dir = /usr/local/taos/taosanode/model/ log-level = DEBUG # draw the query results -draw-result = 1 +draw-result = 0 diff --git a/tools/tdgpt/taosanalytics/algo/anomaly.py b/tools/tdgpt/taosanalytics/algo/anomaly.py index 5f04283c06..801186cd4e 100644 --- a/tools/tdgpt/taosanalytics/algo/anomaly.py +++ b/tools/tdgpt/taosanalytics/algo/anomaly.py @@ -5,6 +5,7 @@ from matplotlib import pyplot as plt from taosanalytics.conf import app_logger, conf from taosanalytics.servicemgmt import loader +from taosanalytics.util import convert_results_to_windows def do_ad_check(input_list, ts_list, algo_name, params): @@ -22,17 +23,19 @@ def do_ad_check(input_list, ts_list, algo_name, params): res = s.execute() - n_error = abs(sum(filter(lambda x: x == -1, res))) + n_error = abs(sum(filter(lambda x: x != s.valid_code, res))) app_logger.log_inst.debug("There are %d in input, and %d anomaly points found: %s", len(input_list), n_error, res) - draw_ad_results(input_list, res, algo_name) - return res + # draw_ad_results(input_list, res, algo_name, s.valid_code) + + ano_window = convert_results_to_windows(res, ts_list, s.valid_code) + return res, ano_window -def draw_ad_results(input_list, res, fig_name): +def draw_ad_results(input_list, res, fig_name, valid_code): """ draw the detected anomaly points """ # not in debug, do not visualize the anomaly detection result @@ -41,9 +44,8 @@ def draw_ad_results(input_list, res, fig_name): plt.clf() for index, val in enumerate(res): - if val != -1: - continue - plt.scatter(index, input_list[index], marker='o', color='r', alpha=0.5, s=100, zorder=3) + if val != valid_code: + plt.scatter(index, input_list[index], marker='o', color='r', alpha=0.5, s=100, zorder=3) plt.plot(input_list, label='sample') plt.savefig(fig_name) diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py index 8a65f88cc3..6a6e13edb2 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/gpt.py +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -10,7 +10,7 @@ from taosanalytics.service import AbstractForecastService class _GPTService(AbstractForecastService): - name = 'td_gpt_fc' + name = 'TDtsfm_1' desc = "internal gpt forecast model based on transformer" def __init__(self): @@ -23,7 +23,6 @@ class _GPTService(AbstractForecastService): self.std = None self.threshold = None self.time_interval = None - self.dir = 'internal-gpt' def execute(self): diff --git a/tools/tdgpt/taosanalytics/app.py b/tools/tdgpt/taosanalytics/app.py index b67fa5f95d..cff5f74447 100644 --- a/tools/tdgpt/taosanalytics/app.py +++ b/tools/tdgpt/taosanalytics/app.py @@ -91,9 +91,7 @@ def handle_ad_request(): # 4. do anomaly detection try: - res_list = do_ad_check(payload[data_index], payload[ts_index], algo, params) - ano_window = convert_results_to_windows(res_list, payload[ts_index]) - + res_list, ano_window = do_ad_check(payload[data_index], payload[ts_index], algo, params) result = {"algo": algo, "option": options, "res": ano_window, "rows": len(ano_window)} app_logger.log_inst.debug("anomaly-detection result: %s", str(result)) diff --git a/tools/tdgpt/taosanalytics/service.py b/tools/tdgpt/taosanalytics/service.py index 79244aae8c..b79d21501a 100644 --- a/tools/tdgpt/taosanalytics/service.py +++ b/tools/tdgpt/taosanalytics/service.py @@ -51,6 +51,7 @@ class AbstractAnomalyDetectionService(AbstractAnalyticsService, ABC): inherent from this class""" def __init__(self): + self.valid_code = 1 super().__init__() self.type = "anomaly-detection" @@ -58,6 +59,12 @@ class AbstractAnomalyDetectionService(AbstractAnalyticsService, ABC): """ check if the input list is empty or None """ return (self.list is None) or (len(self.list) == 0) + def set_params(self, params: dict) -> None: + super().set_params(params) + + if "valid_code" in params: + self.valid_code = int(params["valid_code"]) + class AbstractForecastService(AbstractAnalyticsService, ABC): """abstract forecast service, all forecast algorithms class should be inherent from diff --git a/tools/tdgpt/taosanalytics/test/anomaly_test.py b/tools/tdgpt/taosanalytics/test/anomaly_test.py index bc173cd25b..1f2c7c45c6 100644 --- a/tools/tdgpt/taosanalytics/test/anomaly_test.py +++ b/tools/tdgpt/taosanalytics/test/anomaly_test.py @@ -44,7 +44,7 @@ class AnomalyDetectionTest(unittest.TestCase): s.set_params({"k": 2}) r = s.execute() - draw_ad_results(AnomalyDetectionTest.input_list, r, "ksigma") + draw_ad_results(AnomalyDetectionTest.input_list, r, "ksigma", s.valid_code) self.assertEqual(r[-1], -1) self.assertEqual(len(r), len(AnomalyDetectionTest.input_list)) @@ -64,7 +64,7 @@ class AnomalyDetectionTest(unittest.TestCase): self.assertEqual(1, 0, e) r = s.execute() - draw_ad_results(AnomalyDetectionTest.input_list, r, "iqr") + draw_ad_results(AnomalyDetectionTest.input_list, r, "iqr", s.valid_code) self.assertEqual(r[-1], -1) self.assertEqual(len(r), len(AnomalyDetectionTest.input_list)) @@ -82,7 +82,7 @@ class AnomalyDetectionTest(unittest.TestCase): s.set_params({"alpha": 0.95}) r = s.execute() - draw_ad_results(AnomalyDetectionTest.input_list, r, "grubbs") + draw_ad_results(AnomalyDetectionTest.input_list, r, "grubbs", s.valid_code) self.assertEqual(r[-1], -1) self.assertEqual(len(r), len(AnomalyDetectionTest.input_list)) @@ -100,7 +100,7 @@ class AnomalyDetectionTest(unittest.TestCase): s.set_input_list(AnomalyDetectionTest.input_list, None) r = s.execute() - draw_ad_results(AnomalyDetectionTest.input_list, r, "shesd") + draw_ad_results(AnomalyDetectionTest.input_list, r, "shesd", s.valid_code) self.assertEqual(r[-1], -1) @@ -116,7 +116,7 @@ class AnomalyDetectionTest(unittest.TestCase): s.set_input_list(AnomalyDetectionTest.input_list, None) r = s.execute() - draw_ad_results(AnomalyDetectionTest.input_list, r, "lof") + draw_ad_results(AnomalyDetectionTest.input_list, r, "lof", s.valid_code) self.assertEqual(r[-1], -1) self.assertEqual(r[-2], -1) diff --git a/tools/tdgpt/taosanalytics/util.py b/tools/tdgpt/taosanalytics/util.py index b9b292c3b4..3692560f6c 100644 --- a/tools/tdgpt/taosanalytics/util.py +++ b/tools/tdgpt/taosanalytics/util.py @@ -36,7 +36,7 @@ def validate_pay_load(json_obj): raise ValueError('invalid schema info, data column is missing') -def convert_results_to_windows(result, ts_list): +def convert_results_to_windows(result, ts_list, valid_code): """generate the window according to anomaly detection result""" skey, ekey = -1, -1 wins = [] @@ -45,7 +45,7 @@ def convert_results_to_windows(result, ts_list): return wins for index, val in enumerate(result): - if val == -1: + if val != valid_code: ekey = ts_list[index] if skey == -1: skey = ts_list[index] From 7dd42a32639d9f282064262433b38170bed02834 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Fri, 21 Mar 2025 14:16:03 +0800 Subject: [PATCH 19/38] fix(gpt): not draw results. --- tools/tdgpt/taosanalytics/algo/forecast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tdgpt/taosanalytics/algo/forecast.py b/tools/tdgpt/taosanalytics/algo/forecast.py index e1e321a7b0..4baa92fe15 100644 --- a/tools/tdgpt/taosanalytics/algo/forecast.py +++ b/tools/tdgpt/taosanalytics/algo/forecast.py @@ -34,7 +34,7 @@ def do_forecast(input_list, ts_list, algo_name, params): check_fc_results(res) fc = res["res"] - draw_fc_results(input_list, len(fc) > 2, fc, len(fc[0]), algo_name) + # draw_fc_results(input_list, len(fc) > 2, fc, len(fc[0]), algo_name) return res From 1a426ecdb559dec4fd79e3d26975006688fd5de4 Mon Sep 17 00:00:00 2001 From: Xuefeng Tan <1172915550@qq.com> Date: Fri, 21 Mar 2025 14:49:49 +0800 Subject: [PATCH 20/38] fix: taosadapter version 3.3.6 (#30324) --- cmake/taosadapter_CMakeLists.txt.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/taosadapter_CMakeLists.txt.in b/cmake/taosadapter_CMakeLists.txt.in index ef6ed4af1d..a768b332a6 100644 --- a/cmake/taosadapter_CMakeLists.txt.in +++ b/cmake/taosadapter_CMakeLists.txt.in @@ -2,7 +2,7 @@ # taosadapter ExternalProject_Add(taosadapter GIT_REPOSITORY https://github.com/taosdata/taosadapter.git - GIT_TAG 3.0 + GIT_TAG 3.3.6 SOURCE_DIR "${TD_SOURCE_DIR}/tools/taosadapter" BINARY_DIR "" #BUILD_IN_SOURCE TRUE From 79923d8b41e970a4337942cf5837f527c63698ef Mon Sep 17 00:00:00 2001 From: Jinqing Kuang Date: Fri, 21 Mar 2025 15:00:24 +0800 Subject: [PATCH 21/38] docs(stream): document streaming computation support for virtual tables in user manual (#30319) * docs(stream): document streaming computation support for virtual tables in user manual - Added section to user manual describing streaming computation support for virtual tables - Listed known limitations and behavior when using virtual tables in streaming mode * Update 14-stream.md --------- Co-authored-by: Pan Wei <72057773+dapan1121@users.noreply.github.com> --- docs/en/14-reference/03-taos-sql/14-stream.md | 18 ++++++++++++++++++ docs/zh/14-reference/03-taos-sql/14-stream.md | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/en/14-reference/03-taos-sql/14-stream.md b/docs/en/14-reference/03-taos-sql/14-stream.md index 00e29b0e3d..f8ad80d2a3 100644 --- a/docs/en/14-reference/03-taos-sql/14-stream.md +++ b/docs/en/14-reference/03-taos-sql/14-stream.md @@ -532,6 +532,24 @@ These fields are present only when "windowType" is "Count". #### Fields for Window Invalidation Due to scenarios such as data disorder, updates, or deletions during stream computing, windows that have already been generated might be removed or their results need to be recalculated. In such cases, a notification with the eventType "WINDOW_INVALIDATION" is sent to inform which windows have been invalidated. + For events with "eventType" as "WINDOW_INVALIDATION", the following fields are included: 1. "windowStart": A long integer timestamp representing the start time of the window. 1. "windowEnd": A long integer timestamp representing the end time of the window. + +## Support for Virtual Tables in Stream Computing + +Starting with v3.3.6.0, stream computing can use virtual tables—including virtual regular tables, virtual sub-tables, and virtual super tables—as data sources for computation. The syntax is identical to that for non‑virtual tables. + +However, because the behavior of virtual tables differs from that of non‑virtual tables, the following restrictions apply when using stream computing: + +1. The schema of virtual regular tables/virtual sub-tables involved in stream computing cannot be modified. +1. During stream computing, if the data source corresponding to a column in a virtual table is changed, the stream computation will not pick up the change; it will still read from the old data source. +1. During stream computing, if the original table corresponding to a column in a virtual table is deleted and later a new table with the same name and a column with the same name is created, the stream computation will not read data from the new table. +1. The watermark for stream computing must be 0; otherwise, an error will occur during creation. +1. If the data source for stream computing is a virtual super table, sub-tables that are added after the stream computing task starts will not participate in the computation. +1. The timestamps of different underlying tables in a virtual table may not be completely consistent; merging the data might produce null values, and interpolation is currently not supported. +1. Out-of-order data, updates, or deletions are not handled. In other words, when creating a stream, you cannot specify `ignore update 0` or `ignore expired 0`; otherwise, an error will be reported. +1. Historical data computation is not supported. That is, when creating a stream, you cannot specify `fill_history 1`; otherwise, an error will be reported. +1. The trigger modes MAX_DELAY, CONTINUOUS_WINDOW_CLOSE and FORCE_WINDOW_CLOSE are not supported. +1. The COUNT_WINDOW type is not supported. diff --git a/docs/zh/14-reference/03-taos-sql/14-stream.md b/docs/zh/14-reference/03-taos-sql/14-stream.md index b4c4a76922..a9225a3ab4 100644 --- a/docs/zh/14-reference/03-taos-sql/14-stream.md +++ b/docs/zh/14-reference/03-taos-sql/14-stream.md @@ -527,6 +527,24 @@ CREATE STREAM avg_current_stream FILL_HISTORY 1 #### 窗口失效相关字段 因为流计算过程中会遇到数据乱序、更新、删除等情况,可能造成已生成的窗口被删除,或者结果需要重新计算。此时会向通知地址发送一条 WINDOW_INVALIDATION 的通知,说明哪些窗口已经被删除。 + 这部分是 eventType 为 WINDOW_INVALIDATION 时,event 对象才有的字段。 1. windowStart:长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 1. windowEnd: 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + +## 流式计算对虚拟表的支持 + +从 v3.3.6.0 开始,流计算能够使用虚拟表(包括虚拟普通表、虚拟子表、虚拟超级表)作为数据源进行计算,语法和非虚拟表完全一致。 + +但是虚拟表的行为与非虚拟表存在差异,所以目前在使用流计算对虚拟表进行计算时存在以下限制: + +1. 流计算中涉及的虚拟普通表/虚拟子表的 schema 不允许更改。 +1. 流计算过程中,如果修改虚拟表某一列对应的数据源,对流计算来说不生效。即:流计算仍只读取老的数据源。 +1. 流计算过程中,如果虚拟表某一列对应的原始表被删除,之后新建了同名的表和同名的列,流计算不会读取新表的数据。 +1. 流计算的 watermark 只能是 0,否则创建时就报错。 +1. 如果流计算的数据源是虚拟超级表,流计算任务启动后新增的子表不参与计算。 +1. 虚拟表的不同原始表的时间戳不完全一致,数据合并后可能会产生空值,暂不支持插值处理。 +1. 不处理数据的乱序、更新或删除。即:流创建时不能指定 `ignore update 0` 或者 `ignore expired 0`,否则报错。 +1. 不支持历史数据计算,即:流创建时不能指定 `fill_history 1`,否则报错。 +1. 不支持触发模式:MAX_DELAY, FORCE_WINDOW_CLOSE, CONTINUOUS_WINDOW_CLOSE。 +1. 不支持窗口类型:COUNT_WINDOW。 From f2e9193a953401977aafc581b618a1abe4e9ddc4 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Fri, 21 Mar 2025 17:31:15 +0800 Subject: [PATCH 22/38] fix(gpt): fix error. --- source/libs/executor/src/anomalywindowoperator.c | 2 +- source/libs/executor/src/forecastoperator.c | 2 +- tools/tdgpt/taosanalytics/test/unit_test.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/libs/executor/src/anomalywindowoperator.c b/source/libs/executor/src/anomalywindowoperator.c index 425809e1a2..45d9d90928 100644 --- a/source/libs/executor/src/anomalywindowoperator.c +++ b/source/libs/executor/src/anomalywindowoperator.c @@ -327,7 +327,7 @@ static int32_t anomalyParseJson(SJson* pJson, SArray* pWindows, const char* pId) qError("%s failed to exec forecast, msg:%s", pId, pMsg); } - return TSDB_CODE_ANA_INTERNAL_ERROR; + return TSDB_CODE_ANA_ANODE_RETURN_ERROR; } else if (rows == 0) { return TSDB_CODE_SUCCESS; } diff --git a/source/libs/executor/src/forecastoperator.c b/source/libs/executor/src/forecastoperator.c index b0966a65fe..25052af523 100644 --- a/source/libs/executor/src/forecastoperator.c +++ b/source/libs/executor/src/forecastoperator.c @@ -235,7 +235,7 @@ static int32_t forecastAnalysis(SForecastSupp* pSupp, SSDataBlock* pBlock, const } tjsonDelete(pJson); - return TSDB_CODE_ANA_INTERNAL_ERROR; + return TSDB_CODE_ANA_ANODE_RETURN_ERROR; } if (code < 0) { diff --git a/tools/tdgpt/taosanalytics/test/unit_test.py b/tools/tdgpt/taosanalytics/test/unit_test.py index 5ff1717b5f..1dd9e91969 100644 --- a/tools/tdgpt/taosanalytics/test/unit_test.py +++ b/tools/tdgpt/taosanalytics/test/unit_test.py @@ -17,7 +17,7 @@ class UtilTest(unittest.TestCase): def test_generate_anomaly_window(self): # Test case 1: Normal input wins = convert_results_to_windows([1, 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, -1], - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 1) print(f"The result window is:{wins}") # Assert the number of windows @@ -30,15 +30,15 @@ class UtilTest(unittest.TestCase): self.assertListEqual(wins[1], [12, 12]) # Test case 2: Anomaly input list is empty - wins = convert_results_to_windows([], [1, 2]) + wins = convert_results_to_windows([], [1, 2], 1) self.assertListEqual(wins, []) # Test case 3: Anomaly input list is None - wins = convert_results_to_windows([], None) + wins = convert_results_to_windows([], None, 1) self.assertListEqual(wins, []) # Test case 4: Timestamp list is None - wins = convert_results_to_windows(None, []) + wins = convert_results_to_windows(None, [], 1) self.assertListEqual(wins, []) def test_validate_input_data(self): From 021a7d8cacb63ac195832dd29f0cd864cc5c5df6 Mon Sep 17 00:00:00 2001 From: haoranchen Date: Fri, 21 Mar 2025 18:34:37 +0800 Subject: [PATCH 23/38] ci: add tdgpt .c file into TDengine and TDgpt workflow --- .github/workflows/tdengine-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tdengine-test.yml b/.github/workflows/tdengine-test.yml index 5843ab6ce3..e5e2cdbfaf 100644 --- a/.github/workflows/tdengine-test.yml +++ b/.github/workflows/tdengine-test.yml @@ -13,6 +13,7 @@ on: - 'tools/tdgpt/**' - 'source/libs/executor/src/forecastoperator.c' - 'source/libs/executor/src/anomalywindowoperator.c' + - 'source/dnode/mnode/impl/src/mndAnode.c' - 'include/common/tanalytics.h' - 'source/common/src/tanalytics.c' - 'tests/parallel/tdgpt_cases.task' From e6ca8fec814d97feb27c57c3524c3202e3bafb8c Mon Sep 17 00:00:00 2001 From: haoranchen Date: Fri, 21 Mar 2025 18:35:21 +0800 Subject: [PATCH 24/38] ci: add tdgpt .c file into TDengine and TDgpt workflow --- .github/workflows/tdgpt-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tdgpt-test.yml b/.github/workflows/tdgpt-test.yml index 4bdebdad32..0db9cee11d 100644 --- a/.github/workflows/tdgpt-test.yml +++ b/.github/workflows/tdgpt-test.yml @@ -12,6 +12,7 @@ on: - 'tools/tdgpt/**' - 'source/libs/executor/src/forecastoperator.c' - 'source/libs/executor/src/anomalywindowoperator.c' + - 'source/dnode/mnode/impl/src/mndAnode.c' - 'include/common/tanalytics.h' - 'source/common/src/tanalytics.c' - 'tests/parallel/tdgpt_cases.task' From b09c744fe4f6dbfa938afb4c148759e7156646b3 Mon Sep 17 00:00:00 2001 From: WANG MINGMING Date: Fri, 21 Mar 2025 22:37:32 +0800 Subject: [PATCH 25/38] fix(stream): log level (#30331) --- source/libs/stream/src/streamBackendRocksdb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/libs/stream/src/streamBackendRocksdb.c b/source/libs/stream/src/streamBackendRocksdb.c index aaa38efe31..de66144737 100644 --- a/source/libs/stream/src/streamBackendRocksdb.c +++ b/source/libs/stream/src/streamBackendRocksdb.c @@ -3134,7 +3134,7 @@ rocksdb_iterator_t* streamStateIterCreate(SStreamState* pState, const char* cfKe taosMemoryFree(err); \ code = TSDB_CODE_THIRDPARTY_ERROR; \ } else { \ - stInfo("[InternalERR] write streamState str:%s succ to write to %s, rowValLen:%d, ttlValLen:%d, %p", toString, \ + stDebug("[InternalERR] write streamState str:%s succ to write to %s, rowValLen:%d, ttlValLen:%d, %p", toString, \ funcname, vLen, ttlVLen, wrapper); \ } \ taosMemoryFree(ttlV); \ @@ -4593,7 +4593,7 @@ int32_t streamStatePutBatchOptimize(SStreamState* pState, int32_t cfIdx, rocksdb int32_t klen = ginitDict[cfIdx].enFunc((void*)key, buf); ginitDict[cfIdx].toStrFunc((void*)key, toString); - qInfo("[InternalERR] write cfIdx:%d key:%s vlen:%d", cfIdx, toString, vlen); + stDebug("[InternalERR] write cfIdx:%d key:%s vlen:%d", cfIdx, toString, vlen); char* ttlV = tmpBuf; int32_t ttlVLen = ginitDict[cfIdx].enValueFunc(dst, size, ttl, &ttlV); From 65035649029a6115fceee1d017fdf164aea90af0 Mon Sep 17 00:00:00 2001 From: Jeff Tao Date: Sat, 22 Mar 2025 10:25:18 +0800 Subject: [PATCH 26/38] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e30f4d9869..b2038a0dd4 100644 --- a/README.md +++ b/README.md @@ -54,23 +54,23 @@ English | [简体中文](README-CN.md) | [TDengine Cloud](https://cloud.tdengine # 1. Introduction -TDengine is an open source, high-performance, cloud native [time-series database](https://tdengine.com/tsdb/) optimized for Internet of Things (IoT), Connected Cars, and Industrial IoT. It enables efficient, real-time data ingestion, processing, and monitoring of TB and even PB scale data per day, generated by billions of sensors and data collectors. TDengine differentiates itself from other time-series databases with the following advantages: +TDengine is an open source, high-performance, cloud native and AI powered [time-series database](https://tdengine.com/tsdb/) designed for Internet of Things (IoT), Connected Cars, and Industrial IoT. It enables efficient, real-time data ingestion, processing, and analysis of TB and even PB scale data per day, generated by billions of sensors and data collectors. TDengine differentiates itself from other time-series databases with the following advantages: - **[High Performance](https://tdengine.com/tdengine/high-performance-time-series-database/)**: TDengine is the only time-series database to solve the high cardinality issue to support billions of data collection points while out performing other time-series databases for data ingestion, querying and data compression. -- **[Simplified Solution](https://tdengine.com/tdengine/simplified-time-series-data-solution/)**: Through built-in caching, stream processing and data subscription features, TDengine provides a simplified solution for time-series data processing. It reduces system design complexity and operation costs significantly. +- **[Simplified Solution](https://tdengine.com/tdengine/simplified-time-series-data-solution/)**: Through built-in caching, stream processing, data subscription and AI agent features, TDengine provides a simplified solution for time-series data processing. It reduces system design complexity and operation costs significantly. - **[Cloud Native](https://tdengine.com/tdengine/cloud-native-time-series-database/)**: Through native distributed design, sharding and partitioning, separation of compute and storage, RAFT, support for kubernetes deployment and full observability, TDengine is a cloud native Time-Series Database and can be deployed on public, private or hybrid clouds. +- **[AI Powered](https://tdengine.com/tdengine/tdgpt/)**: TDengine has built in AI agent to link to a variety of time series foundation model, large language model, machine learning and traditional algorithms to provide time series data forecasting, anomly detection, imputation and classification. + - **[Ease of Use](https://tdengine.com/tdengine/easy-time-series-data-platform/)**: For administrators, TDengine significantly reduces the effort to deploy and maintain. For developers, it provides a simple interface, simplified solution and seamless integrations for third party tools. For data users, it gives easy data access. -- **[Easy Data Analytics](https://tdengine.com/tdengine/time-series-data-analytics-made-easy/)**: Through super tables, storage and compute separation, data partitioning by time interval, pre-computation and other means, TDengine makes it easy to explore, format, and get access to data in a highly efficient way. +- **[Easy Data Analytics](https://tdengine.com/tdengine/time-series-data-analytics-made-easy/)**: Through super tables, storage and compute separation, data partitioning by time interval, pre-computation and AI agent, TDengine makes it easy to explore, format, and get access to data in a highly efficient way. -- **[Open Source](https://tdengine.com/tdengine/open-source-time-series-database/)**: TDengine’s core modules, including cluster feature, are all available under open source licenses. It has gathered 19.9k stars on GitHub. There is an active developer community, and over 139k running instances worldwide. +- **[Open Source](https://tdengine.com/tdengine/open-source-time-series-database/)**: TDengine’s core modules, including cluster feature and AI agent, are all available under open source licenses. It has gathered 23.7k stars on GitHub. There is an active developer community, and over 730k running instances worldwide. -For a full list of TDengine competitive advantages, please [check here](https://tdengine.com/tdengine/). The easiest way to experience TDengine is through [TDengine Cloud](https://cloud.tdengine.com). - -For the latest TDengine component TDgpt, please refer to [TDgpt README](./tools/tdgpt/README.md) for details. +For a full list of TDengine competitive advantages, please [check here](https://tdengine.com/tdengine/). The easiest way to experience TDengine is through [TDengine Cloud](https://cloud.tdengine.com). For the latest TDengine component TDgpt, please refer to [TDgpt README](./tools/tdgpt/README.md) for details. # 2. Documentation From 51c9c2fe3d4d0768c179d9ea4d1b3a229918e1ef Mon Sep 17 00:00:00 2001 From: Jeff Tao Date: Sat, 22 Mar 2025 10:35:42 +0800 Subject: [PATCH 27/38] Update README-CN.md --- README-CN.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README-CN.md b/README-CN.md index bea4bc9fe6..f36626c890 100644 --- a/README-CN.md +++ b/README-CN.md @@ -41,21 +41,23 @@ # 1. 简介 -TDengine 是一款开源、高性能、云原生的时序数据库 (Time-Series Database, TSDB)。TDengine 能被广泛运用于物联网、工业互联网、车联网、IT 运维、金融等领域。除核心的时序数据库功能外,TDengine 还提供缓存、数据订阅、流式计算等功能,是一极简的时序数据处理平台,最大程度的减小系统设计的复杂度,降低研发和运营成本。与其他时序数据库相比,TDengine 的主要优势如下: +TDengine 是一款开源、高性能、云原生、AI驱动的时序数据库 (Time-Series Database, TSDB)。TDengine 能被广泛运用于物联网、工业互联网、车联网、IT 运维、金融等领域。除核心的时序数据库功能外,TDengine 还提供缓存、数据订阅、流式计算、AI智能体等功能,是一极简的时序数据处理平台,最大程度的减小系统设计的复杂度,降低研发和运营成本。与其他时序数据库相比,TDengine 的主要优势如下: - **高性能**:通过创新的存储引擎设计,无论是数据写入还是查询,TDengine 的性能比通用数据库快 10 倍以上,也远超其他时序数据库,存储空间不及通用数据库的 1/10。 - **云原生**:通过原生分布式的设计,充分利用云平台的优势,TDengine 提供了水平扩展能力,具备弹性、韧性和可观测性,支持 k8s 部署,可运行在公有云、私有云和混合云上。 -- **极简时序数据平台**:TDengine 内建消息队列、缓存、流式计算等功能,应用无需再集成 Kafka/Redis/HBase/Spark 等软件,大幅降低系统的复杂度,降低应用开发和运营成本。 +- **极简时序数据平台**:TDengine 内建消息队列、缓存、流式计算、AI智能体等功能,应用无需再集成 Kafka/Redis/HBase/Spark 等软件,大幅降低系统的复杂度,降低应用开发和运营成本。 -- **分析能力**:支持 SQL,同时为时序数据特有的分析提供SQL扩展。通过超级表、存储计算分离、分区分片、预计算、自定义函数等技术,TDengine 具备强大的分析能力。 +- **分析能力**:支持 SQL,同时为时序数据特有的分析提供SQL扩展。通过超级表、存储计算分离、分区分片、预计算、自定义函数以及AI Agent等技术,TDengine 具备强大的分析能力。 + +- **AI智能体**:内置时序数据智能体TDgpt, 无缝连接时序数据基础模型、大语言模型、机器学习、传统统计算法等,提供时序数据预测、异常检测、数据补全和数据分类的功能。 - **简单易用**:无任何依赖,安装、集群几秒搞定;提供REST以及各种语言连接器,与众多第三方工具无缝集成;提供命令行程序,便于管理和即席查询;提供各种运维工具。 - **核心开源**:TDengine 的核心代码包括集群功能全部开源,截止到 2022 年 8 月 1 日,全球超过 135.9k 个运行实例,GitHub Star 18.7k,Fork 4.4k,社区活跃。 -了解TDengine高级功能的完整列表,请 [点击](https://tdengine.com/tdengine/)。体验 TDengine 最简单的方式是通过 [TDengine云平台](https://cloud.tdengine.com)。 +了解TDengine高级功能的完整列表,请 [点击](https://tdengine.com/tdengine/)。体验 TDengine 最简单的方式是通过 [TDengine云平台](https://cloud.tdengine.com)。对最新发布的TDengine 组件 TDgpt,请访问[TDgpt README](./tools/tdgpt/README.md) 了解细节。 # 2. 文档 @@ -67,7 +69,7 @@ TDengine 是一款开源、高性能、云原生的时序数据库 (Time-Series # 3. 前置条件 -TDengine 目前可以在 Linux、 Windows、macOS 等平台上安装和运行。任何 OS 的应用也可以选择 taosAdapter 的 RESTful 接口连接服务端 taosd。CPU 支持 X64、ARM64,后续会支持 MIPS64、Alpha64、ARM32、RISC-V 等 CPU 架构。目前不支持使用交叉编译器构建。 +TDengine 目前可以在 Linux 和 macOS 平台上安装和运行 (企业版支持 Windows)。任何 OS 的应用也可以选择 taosAdapter 的 RESTful 接口连接服务端 taosd。CPU 支持 X64、ARM64,后续会支持 MIPS64、Alpha64、ARM32、RISC-V 等 CPU 架构。目前不支持使用交叉编译器构建。 如果你想要编译 taosAdapter 或者 taosKeeper,需要安装 Go 1.18 及以上版本。 From a7d319fca8d672f2f09c437353f3cbaf49b2672b Mon Sep 17 00:00:00 2001 From: Jeff Tao Date: Sat, 22 Mar 2025 10:38:17 +0800 Subject: [PATCH 28/38] Update README.md --- tools/tdgpt/README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tools/tdgpt/README.md b/tools/tdgpt/README.md index d67c6e2f91..cbbda1bf31 100644 --- a/tools/tdgpt/README.md +++ b/tools/tdgpt/README.md @@ -10,8 +10,7 @@ 1. [Testing](#8-testing) 1. [Releasing](#9-releasing) 1. [CI/CD](#10-cicd) -1. [Coverage](#11-coverage) -1. [Contributing](#12-contributing) +1. [Contributing](#11-contributing) # 1. Introduction @@ -93,7 +92,6 @@ The taosanode will be installed as an system service, but will not automatic sta systemctl start taosanoded ``` - ## 6.2 Configure the Service taosanode provides the RESTFul service powered by `uWSGI`. You can config the options to tune the performance by changing the default configuration file `taosanode.ini` located in `/etc/taos`, which is also the configuration directory for `taosd` service. @@ -123,10 +121,7 @@ For the complete list of taosanode Releases, please see Releases. We use Github Actions for CI/CD workflow configuration. Please refer to the workflow definition yaml file in [.github/workflows](../../.github/workflows/) for details. -# 11 Coverage - - -# 12 Contributing +# 11 Contributing Guidelines for contributing to the project: From f980949c3617d42ac93fff13ba0a8860383e27d5 Mon Sep 17 00:00:00 2001 From: Jeff Tao Date: Sat, 22 Mar 2025 10:41:10 +0800 Subject: [PATCH 29/38] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b2038a0dd4..2272210297 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ For contributing/building/testing TDengine Connectors, please check the followin # 3. Prerequisites -At the moment, TDengine server supports running on Linux/Windows/MacOS systems. Any application can also choose the RESTful interface provided by taosAdapter to connect the taosd service. TDengine supports X64/ARM64 CPU, and it will support MIPS64, Alpha64, ARM32, RISC-V and other CPU architectures in the future. Right now we don't support build with cross-compiling environment. +At the moment, TDengine server supports running on Linux/MacOS systems(Windows is supported for Enterprise edition). Any application can also choose the RESTful interface provided by taosAdapter to connect the taosd service. TDengine supports X64/ARM64 CPU, and it will support MIPS64, Alpha64, ARM32, RISC-V and other CPU architectures in the future. Right now we don't support build with cross-compiling environment. If you want to compile taosAdapter or taosKeeper, you need to install Go 1.18 or above. From afe308780b1284db431bfddda3471414e531faa0 Mon Sep 17 00:00:00 2001 From: Jeff Tao Date: Sat, 22 Mar 2025 10:55:00 +0800 Subject: [PATCH 30/38] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2272210297..8ee05b6b97 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ TDengine is an open source, high-performance, cloud native and AI powered [time- - **[Cloud Native](https://tdengine.com/tdengine/cloud-native-time-series-database/)**: Through native distributed design, sharding and partitioning, separation of compute and storage, RAFT, support for kubernetes deployment and full observability, TDengine is a cloud native Time-Series Database and can be deployed on public, private or hybrid clouds. -- **[AI Powered](https://tdengine.com/tdengine/tdgpt/)**: TDengine has built in AI agent to link to a variety of time series foundation model, large language model, machine learning and traditional algorithms to provide time series data forecasting, anomly detection, imputation and classification. +- **[AI Powered](https://tdengine.com/tdengine/tdgpt/)**: Through the built in AI agent TDgpt, TDengine can connect to a variety of time series foundation model, large language model, machine learning and traditional algorithms to provide time series data forecasting, anomly detection, imputation and classification. - **[Ease of Use](https://tdengine.com/tdengine/easy-time-series-data-platform/)**: For administrators, TDengine significantly reduces the effort to deploy and maintain. For developers, it provides a simple interface, simplified solution and seamless integrations for third party tools. For data users, it gives easy data access. From fdb395f628d0bdd6def62a375c7eb15f064ac473 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Sat, 22 Mar 2025 13:24:12 +0800 Subject: [PATCH 31/38] fix(gpt): update the error info. --- source/util/src/terror.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/util/src/terror.c b/source/util/src/terror.c index fd88a53b23..0b5d6df9b6 100644 --- a/source/util/src/terror.c +++ b/source/util/src/terror.c @@ -377,7 +377,7 @@ TAOS_DEFINE_ERROR(TSDB_CODE_ANA_BUF_INVALID_TYPE, "Analysis invalid buffe TAOS_DEFINE_ERROR(TSDB_CODE_ANA_ANODE_RETURN_ERROR, "Analysis failed since anode return error") TAOS_DEFINE_ERROR(TSDB_CODE_ANA_ANODE_TOO_MANY_ROWS, "Analysis failed since too many input rows for anode") TAOS_DEFINE_ERROR(TSDB_CODE_ANA_WN_DATA, "white-noise data not processed") -TAOS_DEFINE_ERROR(TSDB_CODE_ANA_INTERNAL_ERROR, "tdgpt internal error, not processed") +TAOS_DEFINE_ERROR(TSDB_CODE_ANA_INTERNAL_ERROR, "Analysis internal error, not processed") // mnode-sma TAOS_DEFINE_ERROR(TSDB_CODE_MND_SMA_ALREADY_EXIST, "SMA already exists") From d43cebaafddf296de9a1f4cdf34a1aa0f1a70943 Mon Sep 17 00:00:00 2001 From: WANG Xu Date: Sat, 22 Mar 2025 13:36:29 +0800 Subject: [PATCH 32/38] ci: add release build Signed-off-by: WANG Xu --- .github/workflows/tdengine-release-build.yml | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/tdengine-release-build.yml diff --git a/.github/workflows/tdengine-release-build.yml b/.github/workflows/tdengine-release-build.yml new file mode 100644 index 0000000000..ef708a0ac9 --- /dev/null +++ b/.github/workflows/tdengine-release-build.yml @@ -0,0 +1,114 @@ +name: TDengine Release Build + +on: + push: + branches: + - 'main' + - '3.*' + paths-ignore: + - 'docs/**' + - 'packaging/**' + - 'tests/**' + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Run on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - macos-14 + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.18 + + - name: Install dependencies on Linux + if: runner.os == 'Linux' + run: | + sudo apt update -y + sudo apt install -y \ + build-essential \ + cmake \ + gawk \ + libgeos-dev \ + libjansson-dev \ + liblzma-dev \ + libsnappy-dev \ + libssl-dev \ + libz-dev \ + pkg-config \ + zlib1g + + - name: Install dependencies on macOS + if: runner.os == 'macOS' + run: | + brew update + brew install \ + argp-standalone \ + gawk \ + gflags \ + geos \ + jansson \ + openssl \ + pkg-config \ + snappy \ + zlib + + - name: Build and install TDengine + run: | + mkdir debug && cd debug + cmake .. -DBUILD_TOOLS=true \ + -DBUILD_KEEPER=true \ + -DBUILD_HTTP=false \ + -DBUILD_TEST=true \ + -DWEBSOCKET=true \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_DEPENDENCY_TESTS=false + make -j 4 + sudo make install + which taosd + which taosadapter + which taoskeeper + + - name: Statistics ldd + run: | + find ${{ github.workspace }}/debug/build/lib -type f -name "*.so" -print0 | xargs -0 ldd || true + find ${{ github.workspace }}/debug/build/bin -type f -print0 | xargs -0 ldd || true + + - name: Statistics size + run: | + find ${{ github.workspace }}/debug/build/lib -type f -print0 | xargs -0 ls -lhrS + find ${{ github.workspace }}/debug/build/bin -type f -print0 | xargs -0 ls -lhrS + + - name: Start taosd + run: | + cp /etc/taos/taos.cfg ./ + sudo echo "supportVnodes 256" >> taos.cfg + nohup sudo taosd -c taos.cfg & + + - name: Start taosadapter + run: nohup sudo taosadapter & + + - name: Run tests with taosBenchmark + run: | + taosBenchmark -t 10 -n 10 -y + taos -s "select count(*) from test.meters" + + - name: Clean up + if: always() + run: | + if pgrep taosd; then sudo pkill taosd; fi + if pgrep taosadapter; then sudo pkill taosadapter; fi From bcec12896df9ba6fcedea13254bdc0857f2fbef6 Mon Sep 17 00:00:00 2001 From: haoranchen Date: Sat, 22 Mar 2025 15:24:19 +0800 Subject: [PATCH 33/38] fix: add requests pkg in tdgpt install.sh --- tools/tdgpt/script/install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/tdgpt/script/install.sh b/tools/tdgpt/script/install.sh index 9952b7f0af..bd7f001d4d 100755 --- a/tools/tdgpt/script/install.sh +++ b/tools/tdgpt/script/install.sh @@ -400,6 +400,8 @@ function install_anode_venv() { ${csudo}${venvDir}/bin/pip3 install uwsgi ${csudo}${venvDir}/bin/pip3 install torch --index-url https://download.pytorch.org/whl/cpu ${csudo}${venvDir}/bin/pip3 install --upgrade keras + ${csudo}${venvDir}/bin/pip3 install requests + echo -e "Install python library for venv completed!" } From d3f537275f37b718450ce135c68c212319cc5e62 Mon Sep 17 00:00:00 2001 From: Mario Peng <48949600+Pengrongkun@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:13:47 +0800 Subject: [PATCH 34/38] fix(stmt2):stmt2 get fields return wrong when tag is value(3.3.6) (#30308) * fix: add case * fix: stmt2 get fields return wrong when tag is value * enh: replace preCtb bool to flag,handle more situation * fix: some unit test error * diff: add case and fix some problem * fix: remove async test, handle in TD-34077 * fix: review --------- Co-authored-by: Simon Guan --- include/libs/parser/parser.h | 10 ++- source/client/inc/clientStmt.h | 2 +- source/client/src/clientStmt.c | 12 ++-- source/client/src/clientStmt2.c | 19 +++-- source/client/test/stmt2Test.cpp | 99 +++++++++++++++++++------- source/libs/parser/src/parInsertSql.c | 70 +++++++++++------- source/libs/parser/src/parInsertStmt.c | 13 ++-- 7 files changed, 155 insertions(+), 70 deletions(-) diff --git a/include/libs/parser/parser.h b/include/libs/parser/parser.h index 7ce4684f07..4fd6fef81a 100644 --- a/include/libs/parser/parser.h +++ b/include/libs/parser/parser.h @@ -49,10 +49,16 @@ extern "C" { } \ } while (0) +#define HAS_BIND_VALUE ((uint8_t)0x1) +#define IS_FIXED_VALUE ((uint8_t)0x2) +#define USING_CLAUSE ((uint8_t)0x4) +#define IS_FIXED_TAG ((uint8_t)0x8) +#define NO_DATA_USING_CLAUSE ((uint8_t)0x7) + typedef struct SStmtCallback { TAOS_STMT* pStmt; int32_t (*getTbNameFn)(TAOS_STMT*, char**); - int32_t (*setInfoFn)(TAOS_STMT*, STableMeta*, void*, SName*, bool, SHashObj*, SHashObj*, const char*, bool); + int32_t (*setInfoFn)(TAOS_STMT*, STableMeta*, void*, SName*, bool, SHashObj*, SHashObj*, const char*, uint8_t); int32_t (*getExecInfoFn)(TAOS_STMT*, SHashObj**, SHashObj**); } SStmtCallback; @@ -175,7 +181,7 @@ int32_t qBindStmtColsValue(void* pBlock, SArray* pCols, TAOS_MULTI_BIND* bind, c int32_t qBindStmtSingleColValue(void* pBlock, SArray* pCols, TAOS_MULTI_BIND* bind, char* msgBuf, int32_t msgBufLen, int32_t colIdx, int32_t rowNum, void* charsetCxt); int32_t qBuildStmtColFields(void* pDataBlock, int32_t* fieldNum, TAOS_FIELD_E** fields); -int32_t qBuildStmtStbColFields(void* pBlock, void* boundTags, bool hasCtbName, int32_t* fieldNum, +int32_t qBuildStmtStbColFields(void* pBlock, void* boundTags, uint8_t tbNameFlag, int32_t* fieldNum, TAOS_FIELD_ALL** fields); int32_t qBuildStmtTagFields(void* pBlock, void* boundTags, int32_t* fieldNum, TAOS_FIELD_E** fields); int32_t qBindStmtTagsValue(void* pBlock, void* boundTags, int64_t suid, const char* sTableName, char* tName, diff --git a/source/client/inc/clientStmt.h b/source/client/inc/clientStmt.h index 74260c42e0..5d61e8b5d0 100644 --- a/source/client/inc/clientStmt.h +++ b/source/client/inc/clientStmt.h @@ -64,7 +64,7 @@ typedef struct SStmtBindInfo { int32_t sBindLastIdx; int8_t tbType; bool tagsCached; - bool preCtbname; + uint8_t tbNameFlag; void *boundTags; char tbName[TSDB_TABLE_FNAME_LEN]; char tbFName[TSDB_TABLE_FNAME_LEN]; diff --git a/source/client/src/clientStmt.c b/source/client/src/clientStmt.c index 2d9001d248..4cef20056b 100644 --- a/source/client/src/clientStmt.c +++ b/source/client/src/clientStmt.c @@ -238,7 +238,7 @@ int32_t stmtRestoreQueryFields(STscStmt* pStmt) { } */ int32_t stmtUpdateBindInfo(TAOS_STMT* stmt, STableMeta* pTableMeta, void* tags, SName* tbName, const char* sTableName, - bool autoCreateTbl) { + bool autoCreateTbl, uint8_t tbNameFlag) { STscStmt* pStmt = (STscStmt*)stmt; char tbFName[TSDB_TABLE_FNAME_LEN]; int32_t code = tNameExtractFullName(tbName, tbFName); @@ -256,6 +256,7 @@ int32_t stmtUpdateBindInfo(TAOS_STMT* stmt, STableMeta* pTableMeta, void* tags, pStmt->bInfo.tbType = pTableMeta->tableType; pStmt->bInfo.boundTags = tags; pStmt->bInfo.tagsCached = false; + pStmt->bInfo.tbNameFlag = tbNameFlag; tstrncpy(pStmt->bInfo.stbFName, sTableName, sizeof(pStmt->bInfo.stbFName)); return TSDB_CODE_SUCCESS; @@ -271,10 +272,10 @@ int32_t stmtUpdateExecInfo(TAOS_STMT* stmt, SHashObj* pVgHash, SHashObj* pBlockH } int32_t stmtUpdateInfo(TAOS_STMT* stmt, STableMeta* pTableMeta, void* tags, SName* tbName, bool autoCreateTbl, - SHashObj* pVgHash, SHashObj* pBlockHash, const char* sTableName, bool preCtbname) { + SHashObj* pVgHash, SHashObj* pBlockHash, const char* sTableName, uint8_t tbNameFlag) { STscStmt* pStmt = (STscStmt*)stmt; - STMT_ERR_RET(stmtUpdateBindInfo(stmt, pTableMeta, tags, tbName, sTableName, autoCreateTbl)); + STMT_ERR_RET(stmtUpdateBindInfo(stmt, pTableMeta, tags, tbName, sTableName, autoCreateTbl, tbNameFlag)); STMT_ERR_RET(stmtUpdateExecInfo(stmt, pVgHash, pBlockHash)); pStmt->sql.autoCreateTbl = autoCreateTbl; @@ -1729,7 +1730,10 @@ int stmtGetTagFields(TAOS_STMT* stmt, int* nums, TAOS_FIELD_E** fields) { STMT_ERRI_JRET(stmtFetchTagFields(stmt, nums, fields)); _return: - + // compatible with previous versions + if (code == TSDB_CODE_PAR_TABLE_NOT_EXIST && (pStmt->bInfo.tbNameFlag & NO_DATA_USING_CLAUSE) == 0x0) { + code = TSDB_CODE_TSC_STMT_TBNAME_ERROR; + } pStmt->errCode = preCode; return code; diff --git a/source/client/src/clientStmt2.c b/source/client/src/clientStmt2.c index cfd302d895..c4f8702f43 100644 --- a/source/client/src/clientStmt2.c +++ b/source/client/src/clientStmt2.c @@ -193,7 +193,7 @@ static int32_t stmtGetTbName(TAOS_STMT2* stmt, char** tbName) { } static int32_t stmtUpdateBindInfo(TAOS_STMT2* stmt, STableMeta* pTableMeta, void* tags, SName* tbName, - const char* sTableName, bool autoCreateTbl, bool preCtbname) { + const char* sTableName, bool autoCreateTbl, int8_t tbNameFlag) { STscStmt2* pStmt = (STscStmt2*)stmt; char tbFName[TSDB_TABLE_FNAME_LEN]; int32_t code = tNameExtractFullName(tbName, tbFName); @@ -217,7 +217,7 @@ static int32_t stmtUpdateBindInfo(TAOS_STMT2* stmt, STableMeta* pTableMeta, void pStmt->bInfo.boundTags = tags; pStmt->bInfo.tagsCached = false; - pStmt->bInfo.preCtbname = preCtbname; + pStmt->bInfo.tbNameFlag = tbNameFlag; tstrncpy(pStmt->bInfo.stbFName, sTableName, sizeof(pStmt->bInfo.stbFName)); return TSDB_CODE_SUCCESS; @@ -233,10 +233,10 @@ static int32_t stmtUpdateExecInfo(TAOS_STMT2* stmt, SHashObj* pVgHash, SHashObj* } static int32_t stmtUpdateInfo(TAOS_STMT2* stmt, STableMeta* pTableMeta, void* tags, SName* tbName, bool autoCreateTbl, - SHashObj* pVgHash, SHashObj* pBlockHash, const char* sTableName, bool preCtbname) { + SHashObj* pVgHash, SHashObj* pBlockHash, const char* sTableName, uint8_t tbNameFlag) { STscStmt2* pStmt = (STscStmt2*)stmt; - STMT_ERR_RET(stmtUpdateBindInfo(stmt, pTableMeta, tags, tbName, sTableName, autoCreateTbl, preCtbname)); + STMT_ERR_RET(stmtUpdateBindInfo(stmt, pTableMeta, tags, tbName, sTableName, autoCreateTbl, tbNameFlag)); STMT_ERR_RET(stmtUpdateExecInfo(stmt, pVgHash, pBlockHash)); pStmt->sql.autoCreateTbl = autoCreateTbl; @@ -1233,7 +1233,8 @@ static int stmtFetchStbColFields2(STscStmt2* pStmt, int32_t* fieldNum, TAOS_FIEL } STMT_ERRI_JRET( - qBuildStmtStbColFields(*pDataBlock, pStmt->bInfo.boundTags, pStmt->bInfo.preCtbname, fieldNum, fields)); + qBuildStmtStbColFields(*pDataBlock, pStmt->bInfo.boundTags, pStmt->bInfo.tbNameFlag, fieldNum, fields)); + if (pStmt->bInfo.tbType == TSDB_SUPER_TABLE && cleanStb) { pStmt->bInfo.needParse = true; qDestroyStmtDataBlock(*pDataBlock); @@ -2022,7 +2023,9 @@ int stmtParseColFields2(TAOS_STMT2* stmt) { STMT_TYPE_MULTI_INSERT != pStmt->sql.type) { pStmt->bInfo.needParse = false; } - + if (pStmt->sql.stbInterlaceMode && pStmt->sql.siInfo.pDataCtx != NULL) { + pStmt->bInfo.needParse = false; + } if (pStmt->exec.pRequest && STMT_TYPE_QUERY == pStmt->sql.type && pStmt->sql.runTimes) { taos_free_result(pStmt->exec.pRequest); pStmt->exec.pRequest = NULL; @@ -2036,6 +2039,10 @@ int stmtParseColFields2(TAOS_STMT2* stmt) { } _return: + // compatible with previous versions + if (code == TSDB_CODE_PAR_TABLE_NOT_EXIST && (pStmt->bInfo.tbNameFlag & NO_DATA_USING_CLAUSE) == 0x0) { + code = TSDB_CODE_TSC_STMT_TBNAME_ERROR; + } pStmt->errCode = preCode; diff --git a/source/client/test/stmt2Test.cpp b/source/client/test/stmt2Test.cpp index cc54cd55d3..5e50760648 100644 --- a/source/client/test/stmt2Test.cpp +++ b/source/client/test/stmt2Test.cpp @@ -534,7 +534,7 @@ TEST(stmt2Case, insert_ctb_using_get_fields_Test) { printf("support case \n"); // case 1 : test child table already exist { - const char* sql = "INSERT INTO stmt2_testdb_3.t0(ts,b)using stmt2_testdb_3.stb (t1,t2) TAGS(?,?) VALUES (?,?)"; + const char* sql = "INSERT INTO stmt2_testdb_3.t0(ts,b)using stmt2_testdb_3.stb (t1,t2) TAGS(?,?) VALUES(?,?)"; TAOS_FIELD_ALL expectedFields[4] = {{"t1", TSDB_DATA_TYPE_INT, 0, 0, 4, TAOS_FIELD_TAG}, {"t2", TSDB_DATA_TYPE_BINARY, 0, 0, 12, TAOS_FIELD_TAG}, {"ts", TSDB_DATA_TYPE_TIMESTAMP, 2, 0, 8, TAOS_FIELD_COL}, @@ -612,7 +612,7 @@ TEST(stmt2Case, insert_ctb_using_get_fields_Test) { // case 8 : 'db' 'stb' { - const char* sql = "INSERT INTO 'stmt2_testdb_3'.? using 'stmt2_testdb_3'.'stb' (t1,t2) TAGS(?,?) (ts,b)VALUES(?,?)"; + const char* sql = "INSERT INTO 'stmt2_testdb_3'.? using 'stmt2_testdb_3'.'stb' (t1,t2) TAGS(?,?)(ts,b)VALUES(?,?)"; TAOS_FIELD_ALL expectedFields[5] = {{"tbname", TSDB_DATA_TYPE_BINARY, 0, 0, 271, TAOS_FIELD_TBNAME}, {"t1", TSDB_DATA_TYPE_INT, 0, 0, 4, TAOS_FIELD_TAG}, {"t2", TSDB_DATA_TYPE_BINARY, 0, 0, 12, TAOS_FIELD_TAG}, @@ -634,9 +634,20 @@ TEST(stmt2Case, insert_ctb_using_get_fields_Test) { printf("case 9 : %s\n", sql); getFieldsSuccess(taos, sql, expectedFields, 5); } + // case 11: TD-34097 + { + do_query(taos, "use stmt2_testdb_3"); + const char* sql = "INSERT INTO ? using stb (t1,t2) TAGS(1,'abc') (ts,b)VALUES(?,?)"; + TAOS_FIELD_ALL expectedFields[3] = {{"tbname", TSDB_DATA_TYPE_BINARY, 0, 0, 271, TAOS_FIELD_TBNAME}, + {"ts", TSDB_DATA_TYPE_TIMESTAMP, 2, 0, 8, TAOS_FIELD_COL}, + {"b", TSDB_DATA_TYPE_BINARY, 0, 0, 12, TAOS_FIELD_COL}}; + printf("case 11 : %s\n", sql); + getFieldsSuccess(taos, sql, expectedFields, 3); + } // case 10 : test all types { + do_query(taos, "use stmt2_testdb_3"); const char* sql = "insert into ? using all_stb tags(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; TAOS_FIELD_ALL expectedFields[33] = {{"tbname", TSDB_DATA_TYPE_BINARY, 0, 0, 271, TAOS_FIELD_TBNAME}, @@ -711,7 +722,27 @@ TEST(stmt2Case, insert_ctb_using_get_fields_Test) { printf("case 5 : %s\n", sql); getFieldsError(taos, sql, TSDB_CODE_TSC_SQL_SYNTAX_ERROR); } - + // case 6 : mix value and ? + { + do_query(taos, "use stmt2_testdb_3"); + const char* sql = "INSERT INTO ? using stb (t1,t2) TAGS(1,?) (ts,b)VALUES(?,?)"; + printf("case 6 : %s\n", sql); + getFieldsError(taos, sql, TSDB_CODE_TSC_SQL_SYNTAX_ERROR); + } + // case 7 : mix value and ? + { + do_query(taos, "use stmt2_testdb_3"); + const char* sql = "INSERT INTO ? using stb (t1,t2) TAGS(?,?) (ts,b)VALUES(15910606280001,?)"; + printf("case 7 : %s\n", sql); + getFieldsError(taos, sql, TSDB_CODE_TSC_INVALID_OPERATION); + } + // case 8 : mix value and ? + { + do_query(taos, "use stmt2_testdb_3"); + const char* sql = "INSERT INTO ? using stb (t1,t2) TAGS(?,?) (ts,b)VALUES(15910606280001,'abc')"; + printf("case 8 : %s\n", sql); + getFieldsError(taos, sql, TSDB_CODE_TSC_INVALID_OPERATION); + } do_query(taos, "drop database if exists stmt2_testdb_3"); taos_close(taos); } @@ -1002,6 +1033,15 @@ TEST(stmt2Case, stmt2_insert_non_statndard) { printf("stmt2 [%s] : %s\n", "less params", sql); int code = taos_stmt2_prepare(stmt, sql, 0); checkError(stmt, code); + // test get fields + int fieldNum = 0; + TAOS_FIELD_ALL* pFields = NULL; + code = taos_stmt2_get_fields(stmt, &fieldNum, &pFields); + checkError(stmt, code); + ASSERT_EQ(fieldNum, 2); + ASSERT_STREQ(pFields[0].name, "tbname"); + ASSERT_STREQ(pFields[1].name, "ts"); + int total_affect_rows = 0; int t64_len[2] = {sizeof(int64_t), sizeof(int64_t)}; @@ -1024,11 +1064,22 @@ TEST(stmt2Case, stmt2_insert_non_statndard) { code = taos_stmt2_bind_param(stmt, &bindv, -1); checkError(stmt, code); + code = taos_stmt2_get_fields(stmt, &fieldNum, &pFields); + checkError(stmt, code); + ASSERT_EQ(fieldNum, 2); + ASSERT_STREQ(pFields[0].name, "tbname"); + ASSERT_STREQ(pFields[1].name, "ts"); + int affected_rows; taos_stmt2_exec(stmt, &affected_rows); total_affect_rows += affected_rows; - checkError(stmt, code); + + code = taos_stmt2_get_fields(stmt, &fieldNum, &pFields); + checkError(stmt, code); + ASSERT_EQ(fieldNum, 2); + ASSERT_STREQ(pFields[0].name, "tbname"); + ASSERT_STREQ(pFields[1].name, "ts"); } ASSERT_EQ(total_affect_rows, 12); @@ -1959,27 +2010,27 @@ void stmt2_async_test(std::atomic& stop_task) { stop_task = true; } -TEST(stmt2Case, async_order) { - std::atomic stop_task(false); - std::thread t(stmt2_async_test, std::ref(stop_task)); +// TEST(stmt2Case, async_order) { +// std::atomic stop_task(false); +// std::thread t(stmt2_async_test, std::ref(stop_task)); - // 等待 60 秒钟 - auto start_time = std::chrono::steady_clock::now(); - while (!stop_task) { - auto elapsed_time = std::chrono::steady_clock::now() - start_time; - if (std::chrono::duration_cast(elapsed_time).count() > 100) { - if (t.joinable()) { - t.detach(); - } - FAIL() << "Test[stmt2_async_test] timed out"; - break; - } - std::this_thread::sleep_for(std::chrono::seconds(1)); // 每 1s 检查一次 - } - if (t.joinable()) { - t.join(); - } -} +// // 等待 60 秒钟 +// auto start_time = std::chrono::steady_clock::now(); +// while (!stop_task) { +// auto elapsed_time = std::chrono::steady_clock::now() - start_time; +// if (std::chrono::duration_cast(elapsed_time).count() > 100) { +// if (t.joinable()) { +// t.detach(); +// } +// FAIL() << "Test[stmt2_async_test] timed out"; +// break; +// } +// std::this_thread::sleep_for(std::chrono::seconds(1)); // 每 1s 检查一次 +// } +// if (t.joinable()) { +// t.join(); +// } +// } TEST(stmt2Case, rowformat_bind) { TAOS* taos = taos_connect("localhost", "root", "taosdata", "", 0); diff --git a/source/libs/parser/src/parInsertSql.c b/source/libs/parser/src/parInsertSql.c index 81129a6f8f..25e8ccc1c6 100644 --- a/source/libs/parser/src/parInsertSql.c +++ b/source/libs/parser/src/parInsertSql.c @@ -32,7 +32,7 @@ typedef struct SInsertParseContext { bool needTableTagVal; bool needRequest; // whether or not request server bool isStmtBind; // whether is stmt bind - bool preCtbname; + uint8_t stmtTbNameFlag; } SInsertParseContext; typedef int32_t (*_row_append_fn_t)(SMsgBuf* pMsgBuf, const void* value, int32_t len, void* param); @@ -993,6 +993,10 @@ static int32_t parseTagsClauseImpl(SInsertParseContext* pCxt, SVnodeModifyOpStmt code = buildSyntaxErrMsg(&pCxt->msg, "? only used in stmt", token.z); break; } + if (pTagVals->size != 0) { + code = buildSyntaxErrMsg(&pCxt->msg, "no mix usage for ? and tag values", token.z); + break; + } continue; } @@ -1026,6 +1030,10 @@ static int32_t parseTagsClauseImpl(SInsertParseContext* pCxt, SVnodeModifyOpStmt pTag = NULL; } + if (code == TSDB_CODE_SUCCESS && !isParseBindParam) { + pCxt->stmtTbNameFlag |= IS_FIXED_TAG; + } + _exit: for (int32_t i = 0; i < taosArrayGetSize(pTagVals); ++i) { STagVal* p = (STagVal*)TARRAY_GET_ELEM(pTagVals, i); @@ -1416,6 +1424,7 @@ static int32_t parseUsingTableName(SInsertParseContext* pCxt, SVnodeModifyOpStmt return getTargetTableSchema(pCxt, pStmt); } pStmt->usingTableProcessing = true; + pCxt->stmtTbNameFlag |= USING_CLAUSE; // pStmt->pSql -> stb_name [(tag1_name, ...) pStmt->pSql += index; int32_t code = parseDuplicateUsingClause(pCxt, pStmt, &pCxt->usingDuplicateTable); @@ -1465,7 +1474,7 @@ static int32_t getTableDataCxt(SInsertParseContext* pCxt, SVnodeModifyOpStmt* pS char tbFName[TSDB_TABLE_FNAME_LEN]; int32_t code = 0; - if (pCxt->preCtbname) { + if ((pCxt->stmtTbNameFlag & NO_DATA_USING_CLAUSE) == USING_CLAUSE) { tstrncpy(pStmt->targetTableName.tname, pStmt->usingTableName.tname, sizeof(pStmt->targetTableName.tname)); tstrncpy(pStmt->targetTableName.dbname, pStmt->usingTableName.dbname, sizeof(pStmt->targetTableName.dbname)); pStmt->targetTableName.type = TSDB_SUPER_TABLE; @@ -2764,6 +2773,7 @@ static int32_t checkTableClauseFirstToken(SInsertParseContext* pCxt, SVnodeModif } if (TK_NK_QUESTION == pTbName->type) { + pCxt->stmtTbNameFlag &= ~IS_FIXED_VALUE; pCxt->isStmtBind = true; if (NULL == pCxt->pComCxt->pStmtCb) { return buildSyntaxErrMsg(&pCxt->msg, "? only used in stmt", pTbName->z); @@ -2772,14 +2782,15 @@ static int32_t checkTableClauseFirstToken(SInsertParseContext* pCxt, SVnodeModif char* tbName = NULL; int32_t code = (*pCxt->pComCxt->pStmtCb->getTbNameFn)(pCxt->pComCxt->pStmtCb->pStmt, &tbName); if (TSDB_CODE_SUCCESS == code) { + pCxt->stmtTbNameFlag |= HAS_BIND_VALUE; pTbName->z = tbName; pTbName->n = strlen(tbName); - } else if (code == TSDB_CODE_TSC_STMT_TBNAME_ERROR) { - pCxt->preCtbname = true; - code = TSDB_CODE_SUCCESS; - } else { - return code; } + if (code == TSDB_CODE_TSC_STMT_TBNAME_ERROR) { + pCxt->stmtTbNameFlag &= ~HAS_BIND_VALUE; + code = TSDB_CODE_SUCCESS; + } + return code; } if (TK_NK_ID != pTbName->type && TK_NK_STRING != pTbName->type && TK_NK_QUESTION != pTbName->type) { @@ -2788,26 +2799,34 @@ static int32_t checkTableClauseFirstToken(SInsertParseContext* pCxt, SVnodeModif // db.? situation,ensure that the only thing following the '.' mark is '?' char* tbNameAfterDbName = strnchr(pTbName->z, '.', pTbName->n, true); - if ((tbNameAfterDbName != NULL) && (*(tbNameAfterDbName + 1) == '?')) { - char* tbName = NULL; - if (NULL == pCxt->pComCxt->pStmtCb) { - return buildSyntaxErrMsg(&pCxt->msg, "? only used in stmt", pTbName->z); - } - int32_t code = (*pCxt->pComCxt->pStmtCb->getTbNameFn)(pCxt->pComCxt->pStmtCb->pStmt, &tbName); - if (code != TSDB_CODE_SUCCESS) { - pCxt->preCtbname = true; + if (tbNameAfterDbName != NULL) { + if (*(tbNameAfterDbName + 1) == '?') { + pCxt->stmtTbNameFlag &= ~IS_FIXED_VALUE; + char* tbName = NULL; + if (NULL == pCxt->pComCxt->pStmtCb) { + return buildSyntaxErrMsg(&pCxt->msg, "? only used in stmt", pTbName->z); + } + int32_t code = (*pCxt->pComCxt->pStmtCb->getTbNameFn)(pCxt->pComCxt->pStmtCb->pStmt, &tbName); + if (TSDB_CODE_SUCCESS == code) { + pCxt->stmtTbNameFlag |= HAS_BIND_VALUE; + pTbName->z = tbName; + pTbName->n = strlen(tbName); + } + if (code == TSDB_CODE_TSC_STMT_TBNAME_ERROR) { + pCxt->stmtTbNameFlag &= ~HAS_BIND_VALUE; + code = TSDB_CODE_SUCCESS; + } } else { - pTbName->z = tbName; - pTbName->n = strlen(tbName); - } - } - - if (pCxt->isStmtBind) { - if (TK_NK_ID == pTbName->type || (tbNameAfterDbName != NULL && *(tbNameAfterDbName + 1) != '?')) { - // In SQL statements, the table name has already been specified. + pCxt->stmtTbNameFlag |= IS_FIXED_VALUE; parserWarn("QID:0x%" PRIx64 ", table name is specified in sql, ignore the table name in bind param", pCxt->pComCxt->requestId); + *pHasData = true; } + return TSDB_CODE_SUCCESS; + } + + if (TK_NK_ID == pTbName->type) { + pCxt->stmtTbNameFlag |= IS_FIXED_VALUE; } *pHasData = true; @@ -2824,7 +2843,7 @@ static int32_t setStmtInfo(SInsertParseContext* pCxt, SVnodeModifyOpStmt* pStmt) SStmtCallback* pStmtCb = pCxt->pComCxt->pStmtCb; int32_t code = (*pStmtCb->setInfoFn)(pStmtCb->pStmt, pStmt->pTableMeta, tags, &pStmt->targetTableName, pStmt->usingTableProcessing, pStmt->pVgroupsHashObj, pStmt->pTableBlockHashObj, - pStmt->usingTableName.tname, pCxt->preCtbname); + pStmt->usingTableName.tname, pCxt->stmtTbNameFlag); memset(&pCxt->tags, 0, sizeof(pCxt->tags)); pStmt->pVgroupsHashObj = NULL; @@ -2880,9 +2899,6 @@ static int32_t parseInsertBody(SInsertParseContext* pCxt, SVnodeModifyOpStmt* pS if (TSDB_CODE_SUCCESS == code && hasData) { code = parseInsertTableClause(pCxt, pStmt, &token); } - if (TSDB_CODE_PAR_TABLE_NOT_EXIST == code && pCxt->preCtbname) { - code = TSDB_CODE_TSC_STMT_TBNAME_ERROR; - } } if (TSDB_CODE_SUCCESS == code && !pCxt->missCache) { diff --git a/source/libs/parser/src/parInsertStmt.c b/source/libs/parser/src/parInsertStmt.c index 9bb98eb223..2dc9e96264 100644 --- a/source/libs/parser/src/parInsertStmt.c +++ b/source/libs/parser/src/parInsertStmt.c @@ -1070,10 +1070,11 @@ int32_t buildBoundFields(int32_t numOfBound, int16_t* boundColumns, SSchema* pSc } int32_t buildStbBoundFields(SBoundColInfo boundColsInfo, SSchema* pSchema, int32_t* fieldNum, TAOS_FIELD_ALL** fields, - STableMeta* pMeta, void* boundTags, bool preCtbname) { + STableMeta* pMeta, void* boundTags, uint8_t tbNameFlag) { SBoundColInfo* tags = (SBoundColInfo*)boundTags; - bool hastag = tags != NULL; - int32_t numOfBound = boundColsInfo.numOfBound + (preCtbname ? 1 : 0); + bool hastag = (tags != NULL) && !(tbNameFlag & IS_FIXED_TAG); + int32_t numOfBound = + boundColsInfo.numOfBound + ((tbNameFlag & IS_FIXED_VALUE) == 0 && (tbNameFlag & USING_CLAUSE) != 0 ? 1 : 0); if (hastag) { numOfBound += tags->mixTagsCols ? 0 : tags->numOfBound; } @@ -1084,7 +1085,7 @@ int32_t buildStbBoundFields(SBoundColInfo boundColsInfo, SSchema* pSchema, int32 return terrno; } - if (preCtbname && numOfBound != boundColsInfo.numOfBound) { + if ((tbNameFlag & IS_FIXED_VALUE) == 0 && (tbNameFlag & USING_CLAUSE) != 0) { (*fields)[idx].field_type = TAOS_FIELD_TBNAME; tstrncpy((*fields)[idx].name, "tbname", sizeof((*fields)[idx].name)); (*fields)[idx].type = TSDB_DATA_TYPE_BINARY; @@ -1188,7 +1189,7 @@ int32_t qBuildStmtColFields(void* pBlock, int32_t* fieldNum, TAOS_FIELD_E** fiel return TSDB_CODE_SUCCESS; } -int32_t qBuildStmtStbColFields(void* pBlock, void* boundTags, bool preCtbname, int32_t* fieldNum, +int32_t qBuildStmtStbColFields(void* pBlock, void* boundTags, uint8_t tbNameFlag, int32_t* fieldNum, TAOS_FIELD_ALL** fields) { STableDataCxt* pDataBlock = (STableDataCxt*)pBlock; SSchema* pSchema = getTableColumnSchema(pDataBlock->pMeta); @@ -1202,7 +1203,7 @@ int32_t qBuildStmtStbColFields(void* pBlock, void* boundTags, bool preCtbname, i } CHECK_CODE(buildStbBoundFields(pDataBlock->boundColsInfo, pSchema, fieldNum, fields, pDataBlock->pMeta, boundTags, - preCtbname)); + tbNameFlag)); return TSDB_CODE_SUCCESS; } From 6c2c4d4fef34ec81d683df7c2635ad809cbf30dc Mon Sep 17 00:00:00 2001 From: Simon Guan Date: Sat, 22 Mar 2025 23:20:49 +0800 Subject: [PATCH 35/38] docs: format doc (#30335) --- docs/en/14-reference/03-taos-sql/20-keywords.md | 3 ++- docs/zh/06-advanced/03-stream.md | 2 +- docs/zh/14-reference/01-components/01-taosd.md | 4 ++-- docs/zh/14-reference/03-taos-sql/01-data-type.md | 12 ++++++------ docs/zh/14-reference/03-taos-sql/10-function.md | 10 +++++----- docs/zh/14-reference/03-taos-sql/20-keywords.md | 6 ++++-- docs/zh/14-reference/05-connector/10-cpp.mdx | 2 +- source/libs/catalog/src/ctgDbg.c | 2 +- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/en/14-reference/03-taos-sql/20-keywords.md b/docs/en/14-reference/03-taos-sql/20-keywords.md index d1bd59298f..c9a3033191 100644 --- a/docs/en/14-reference/03-taos-sql/20-keywords.md +++ b/docs/en/14-reference/03-taos-sql/20-keywords.md @@ -99,6 +99,7 @@ The list of keywords is as follows: | CONSUMER | | | CONSUMERS | | | CONTAINS | | +| CONTINUOUS_WINDOW_CLOSE | 3.3.6.0+ | | COPY | | | COUNT | | | COUNT_WINDOW | | @@ -113,7 +114,7 @@ The list of keywords is as follows: | DATABASE | | | DATABASES | | | DBS | | -| DECIMAL | | +| DECIMAL | 3.3.6.0+ | | DEFERRED | | | DELETE | | | DELETE_MARK | | diff --git a/docs/zh/06-advanced/03-stream.md b/docs/zh/06-advanced/03-stream.md index eb1d3a5150..0dfecace76 100644 --- a/docs/zh/06-advanced/03-stream.md +++ b/docs/zh/06-advanced/03-stream.md @@ -136,7 +136,7 @@ create stream if not exists count_history_s fill_history 1 into count_history as ```sql create stream if not exists continuous_query_s trigger force_window_close into continuous_query as select count(*) from power.meters interval(10s) sliding(1s) ``` -5. CONTINUOUS_WINDOW_CLOSE:窗口关闭时输出结果。修改、删除数据,并不会立即触发重算,每等待 rec_time_val 时长,会进行周期性重算。如果不指定 rec_time_val,那么重算周期是60分钟。如果重算的时间长度超过 rec_time_val,在本次重算后,自动开启下一次重算。该模式当前只支持 INTERVAL 窗口。如果使用 FILL,需要配置 adapter的相关信息:adapterFqdn、adapterPort、adapterToken。adapterToken 为 `{username}:{password}` 经过 Base64 编码之后的字符串,例如 `root:taosdata` 编码后为 `cm9vdDp0YW9zZGF0YQ==`。 +5. CONTINUOUS_WINDOW_CLOSE:窗口关闭时输出结果。修改、删除数据,并不会立即触发重算,每等待 rec_time_val 时长,会进行周期性重算。如果不指定 rec_time_val,那么重算周期是 60 分钟。如果重算的时间长度超过 rec_time_val,在本次重算后,自动开启下一次重算。该模式当前只支持 INTERVAL 窗口。如果使用 FILL,需要配置 adapter的相关信息:adapterFqdn、adapterPort、adapterToken。adapterToken 为 `{username}:{password}` 经过 Base64 编码之后的字符串,例如 `root:taosdata` 编码后为 `cm9vdDp0YW9zZGF0YQ==`。 窗口关闭是由事件时间决定的,如事件流中断、或持续延迟,此时事件时间无法更新,可能导致无法得到最新的计算结果。 diff --git a/docs/zh/14-reference/01-components/01-taosd.md b/docs/zh/14-reference/01-components/01-taosd.md index 5cf2d555d4..507617bea8 100644 --- a/docs/zh/14-reference/01-components/01-taosd.md +++ b/docs/zh/14-reference/01-components/01-taosd.md @@ -1096,14 +1096,14 @@ charset 的有效值是 UTF-8。 - 支持版本:v3.3.6.0 引入 #### adapterFqdn -- 说明:taosadapter服务的地址 `内部参数` +- 说明:taosAdapter 服务的地址 `内部参数` - 类型:fqdn - 默认值:localhost - 动态修改:不支持 - 支持版本:v3.3.6.0 引入 #### adapterPort -- 说明:taosadapter服务的端口号 `内部参数` +- 说明:taosAdapter 服务的端口号 `内部参数` - 类型:整数 - 默认值:6041 - 最小值:1 diff --git a/docs/zh/14-reference/03-taos-sql/01-data-type.md b/docs/zh/14-reference/03-taos-sql/01-data-type.md index ac0883e2ef..0f39b1fd7b 100644 --- a/docs/zh/14-reference/03-taos-sql/01-data-type.md +++ b/docs/zh/14-reference/03-taos-sql/01-data-type.md @@ -64,16 +64,16 @@ CREATE DATABASE db_name PRECISION 'ns'; ::: -### DECIMAL数据类型 -`DECIMAL`数据类型用于高精度数值存储, 自版本3.3.6开始支持, 定义语法: DECIMAL(18, 2), DECIMAL(38, 10), 其中需要指定两个参数, 分别为`precision`和`scale`. `precision`是指最大支持的有效数字个数, `scale`是指最大支持的小数位数. 如DECIMAL(8, 4), 可表示范围即[-9999.9999, 9999.9999]. 定义DECIMAL数据类型时, `precision`范围为: [1,38], scale的范围为: [0,precision], scale为0时, 仅表示整数. 也可以不指定scale, 默认为0, 如DECIMAL(18), 与DECIMAL(18,0)相同。 +### DECIMAL 数据类型 +`DECIMAL` 数据类型用于高精度数值存储,自 v3.3.6.0 开始支持, 定义语法:`DECIMAL(18, 2)`,`DECIMAL(38, 10)`, 其中需要指定两个参数, 分别为 `precision` 和 `scale`。`precision` 是指最大支持的有效数字个数,`scale` 是指最大支持的小数位数。如 `DECIMAL(8, 4)`,可表示范围即 `[-9999.9999, 9999.9999]`。定义 DECIMAL 数据类型时,`precision` 范围为:`[1, 38]`, scale 的范围为:`[0, precision]`,scale 为 0 时,仅表示整数。也可以不指定 scale,默认为 0,例如 `DECIMAL(18)`,与 `DECIMAL(18, 0)` 相同。 -当`precision`值不大于18时, 内部使用8字节存储(DECIMAL64), 当precision范围为(18, 38]时, 使用16字节存储(DECIMAL). SQL中写入DECIMAL类型数据时, 可直接使用数值写入, 当写入值大于类型可表示的最大值时会报DECIMAL_OVERFLOW错误, 当未大于类型表示的最大值, 但小数位数超过SCALE时, 会自动四舍五入处理, 如定义类型DECIMAL(10, 2), 写入10.987, 则实际存储值为10.99。 +当 `precision` 值不大于 18 时, 内部使用 8 字节存储(DECIMAL64), 当 `precision` 范围为 `(18, 38]` 时, 使用 16 字节存储(DECIMAL)。SQL 中写入 DECIMAL 类型数据时,可直接使用数值写入,当写入值大于类型可表示的最大值时会报 DECIMAL_OVERFLOW 错误, 当未大于类型表示的最大值, 但小数位数超过 SCALE 时, 会自动四舍五入处理。如定义类型 DECIMAL(10, 2),写入10.987,则实际存储值为 10.99 。 -DECIMAL类型仅支持普通列, 暂不支持tag列. DECIMAL类型只支持SQL写入, 暂不支持stmt写入和schemeless写入。 +DECIMAL 类型仅支持普通列,暂不支持 tag 列。DECIMAL 类型只支持 SQL 写入,暂不支持 stmt 写入和 schemeless 写入。 -整数类型和DECIMAL类型操作时, 会将整数类型转换为DECIMAL类型再进行计算. DECIMAL类型与DOUBLE/FLOAT/VARCHAR/NCHAR等类型计算时, 转换为DOUBLE类型进行计算. +整数类型和 DECIMAL 类型操作时, 会将整数类型转换为 DECIMAL 类型再进行计算。DECIMAL 类型与 DOUBLE/FLOAT/VARCHAR/NCHAR 等类型计算时, 转换为 DOUBLE 类型进行计算。 -查询DECIMAL类型表达式时, 若计算的中间结果超出当前类型可表示的最大值时, 报DECIMAL OVERFLOW错误. +查询 DECIMAL 类型表达式时,若计算的中间结果超出当前类型可表示的最大值时,报 DECIMAL OVERFLOW 错误. ## 常量 diff --git a/docs/zh/14-reference/03-taos-sql/10-function.md b/docs/zh/14-reference/03-taos-sql/10-function.md index 85d9434e04..d0cec833a4 100644 --- a/docs/zh/14-reference/03-taos-sql/10-function.md +++ b/docs/zh/14-reference/03-taos-sql/10-function.md @@ -1137,7 +1137,7 @@ CAST(expr AS type_name) - 字符串类型转换数值类型时可能出现的无效字符情况,例如 "a" 可能转为 0,但不会报错。 - 转换到数值类型时,数值大于 type_name 可表示的范围时,则会溢出,但不会报错。 - 转换到字符串类型时,如果转换后长度超过 type_name 中指定的长度,则会截断,但不会报错。 -- DECIMAL类型不支持与JSON,VARBINARY,GEOMERTY类型的互转. +- DECIMAL 类型不支持与 JSON、VARBINARY、GEOMERTY 类型的互转。 #### TO_CHAR @@ -1619,13 +1619,13 @@ AVG(expr) **功能说明**:统计指定字段的平均值。 -**返回数据类型**:DOUBLE, DECIMAL。 +**返回数据类型**:DOUBLE、DECIMAL。 **适用数据类型**:数值类型。 **适用于**:表和超级表。 -**说明**: 当输入类型为DECIMAL类型时, 输出类型也为DECIMAL类型, 输出的precision和scale大小符合数据类型章节中的描述规则, 通过计算SUM类型和UINT64的除法得到结果类型, 若SUM的结果导致DECIMAL类型溢出, 则报DECIMAL OVERFLOW错误。 +**说明**: 当输入类型为 DECIMAL 类型时,输出类型也为 DECIMAL 类型,输出的 precision 和 scale 大小符合数据类型章节中的描述规则,通过计算 SUM 类型和 UINT64 的除法得到结果类型,若 SUM 的结果导致 DECIMAL 类型溢出, 则报 DECIMAL OVERFLOW 错误。 ### COUNT @@ -1808,13 +1808,13 @@ SUM(expr) **功能说明**:统计表/超级表中某列的和。 -**返回数据类型**:DOUBLE、BIGINT,DECIMAL。 +**返回数据类型**:DOUBLE、BIGINT、DECIMAL。 **适用数据类型**:数值类型。 **适用于**:表和超级表。 -**说明**: 输入类型为DECIMAL类型时, 输出类型为DECIMAL(38, scale), precision为当前支持的最大值, scale为输入类型的scale, 若SUM的结果溢出时, 报DECIMAL OVERFLOW错误. +**说明**: 输入类型为 DECIMAL 类型时,输出类型为 DECIMAL(38, scale) ,precision 为当前支持的最大值,scale 为输入类型的 scale,若 SUM 的结果溢出时,报 DECIMAL OVERFLOW 错误. ### VAR_POP diff --git a/docs/zh/14-reference/03-taos-sql/20-keywords.md b/docs/zh/14-reference/03-taos-sql/20-keywords.md index a2427c99c5..270b3d830d 100644 --- a/docs/zh/14-reference/03-taos-sql/20-keywords.md +++ b/docs/zh/14-reference/03-taos-sql/20-keywords.md @@ -35,6 +35,7 @@ description: TDengine 保留关键字的详细列表 | AS | | | ASC | | | ASOF | | +| ASYNC | 3.3.6.0+ | | AT_ONCE | | | ATTACH | | | AUTO | 3.3.5.0+ | @@ -96,6 +97,7 @@ description: TDengine 保留关键字的详细列表 | CONSUMER | | | CONSUMERS | | | CONTAINS | | +| CONTINUOUS_WINDOW_CLOSE | 3.3.6.0+ | | COPY | | | COUNT | | | COUNT_WINDOW | | @@ -109,7 +111,7 @@ description: TDengine 保留关键字的详细列表 | DATABASE | | | DATABASES | | | DBS | | -| DECIMAL | | +| DECIMAL | 3.3.6.0+ | | DEFERRED | | | DELETE | | | DELETE_MARK | | @@ -239,7 +241,7 @@ description: TDengine 保留关键字的详细列表 | LEADER | | | LEADING | | | LEFT | | -| LEVEL | 3.3.0.0 到 3.3.2.11 的所有版本 | +| LEVEL | 3.3.0.0 - 3.3.2.11 | | LICENCES | | | LIKE | | | LIMIT | | diff --git a/docs/zh/14-reference/05-connector/10-cpp.mdx b/docs/zh/14-reference/05-connector/10-cpp.mdx index 14dfe45a6c..cee40b02db 100644 --- a/docs/zh/14-reference/05-connector/10-cpp.mdx +++ b/docs/zh/14-reference/05-connector/10-cpp.mdx @@ -827,7 +827,7 @@ TDengine 客户端驱动的版本号与 TDengine 服务端的版本号是一一 - **返回值**:非 `NULL`:成功,返回一个指向 TAOS_FIELD 结构体的指针,每个元素代表一列的元数据。`NULL`:失败。 - `TAOS_FIELD_E *taos_fetch_fields_e(TAOS_RES *res)` - - **接口说明**:获取查询结果集每列数据的属性(列的名称、列的数据类型、列的长度),与 `taos_num_fields()` 配合使用,可用来解析 `taos_fetch_row()` 返回的一个元组(一行)的数据。TAOS_FIELD_E中除了TAOS_FIELD的基本信息外, 还包括了类型的`precision`和`scale`信息。 + - **接口说明**:获取查询结果集每列数据的属性(列的名称、列的数据类型、列的长度),与 `taos_num_fields()` 配合使用,可用来解析 `taos_fetch_row()` 返回的一个元组(一行)的数据。TAOS_FIELD_E中 除了 TAOS_FIELD 的基本信息外, 还包括了类型的 `precision` 和 `scale` 信息。 - **参数说明**: - res:[入参] 结果集。 - **返回值**:非 `NULL`:成功,返回一个指向 TAOS_FIELD_E 结构体的指针,每个元素代表一列的元数据。`NULL`:失败。 diff --git a/source/libs/catalog/src/ctgDbg.c b/source/libs/catalog/src/ctgDbg.c index a1512f8fd9..9ea06b9ede 100644 --- a/source/libs/catalog/src/ctgDbg.c +++ b/source/libs/catalog/src/ctgDbg.c @@ -538,7 +538,7 @@ void ctgdShowDBCache(SCatalog *pCtg, SHashObj *dbHash) { "] %s: cfgVersion:%d, numOfVgroups:%d, numOfStables:%d, buffer:%d, cacheSize:%d, pageSize:%d, pages:%d" ", daysPerFile:%d, daysToKeep0:%d, daysToKeep1:%d, daysToKeep2:%d, minRows:%d, maxRows:%d, walFsyncPeriod:%d" ", hashPrefix:%d, hashSuffix:%d, walLevel:%d, precision:%d, compression:%d, replications:%d, strict:%d" - ", cacheLast:%d, tsdbPageSize:%d, walRetentionPeriod:%d, walRollPeriod:%d, walRetentionSize:%" PRId64 "" + ", cacheLast:%d, tsdbPageSize:%d, walRetentionPeriod:%d, walRollPeriod:%d, walRetentionSize:%" PRId64 ", walSegmentSize:%" PRId64 ", numOfRetensions:%d, schemaless:%d, sstTrigger:%d", i, (int32_t)len, dbFName, dbCache->dbId, dbCache->deleted ? "deleted" : "", pCfg->cfgVersion, pCfg->numOfVgroups, pCfg->numOfStables, pCfg->buffer, From 82ffeb82a95fddb32a794329af609a7619e618de Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Sun, 23 Mar 2025 13:20:24 +0800 Subject: [PATCH 36/38] fix(gpt): fix the bug for algo name case sensitive (#30362) --- include/common/tmsg.h | 14 ++++++------ source/common/src/msg/tmsg.c | 15 ++++++++----- source/dnode/mgmt/mgmt_dnode/src/dmHandle.c | 6 ++--- source/dnode/mgmt/node_mgmt/src/dmTransport.c | 10 ++++----- source/dnode/mnode/impl/src/mndAnode.c | 12 +++++----- source/libs/executor/src/forecastoperator.c | 1 + tools/tdgpt/taosanalytics/algo/fc/arima.py | 6 ++--- tools/tdgpt/taosanalytics/algo/fc/gpt.py | 10 ++++----- .../taosanalytics/algo/fc/holtwinters.py | 6 ++--- tools/tdgpt/taosanalytics/algo/fc/lstm.py | 2 +- tools/tdgpt/taosanalytics/algo/forecast.py | 2 +- tools/tdgpt/taosanalytics/app.py | 2 +- tools/tdgpt/taosanalytics/service.py | 12 +++++----- .../tdgpt/taosanalytics/test/forecast_test.py | 22 +++++++++---------- 14 files changed, 62 insertions(+), 58 deletions(-) diff --git a/include/common/tmsg.h b/include/common/tmsg.h index c295c40c1e..75a800f1c3 100644 --- a/include/common/tmsg.h +++ b/include/common/tmsg.h @@ -1257,18 +1257,18 @@ int32_t tDeserializeRetrieveIpWhite(void* buf, int32_t bufLen, SRetrieveIpWhiteR typedef struct { int32_t dnodeId; int64_t analVer; -} SRetrieveAnalAlgoReq; +} SRetrieveAnalyticsAlgoReq; typedef struct { int64_t ver; SHashObj* hash; // algoname:algotype -> SAnalUrl -} SRetrieveAnalAlgoRsp; +} SRetrieveAnalyticAlgoRsp; -int32_t tSerializeRetrieveAnalAlgoReq(void* buf, int32_t bufLen, SRetrieveAnalAlgoReq* pReq); -int32_t tDeserializeRetrieveAnalAlgoReq(void* buf, int32_t bufLen, SRetrieveAnalAlgoReq* pReq); -int32_t tSerializeRetrieveAnalAlgoRsp(void* buf, int32_t bufLen, SRetrieveAnalAlgoRsp* pRsp); -int32_t tDeserializeRetrieveAnalAlgoRsp(void* buf, int32_t bufLen, SRetrieveAnalAlgoRsp* pRsp); -void tFreeRetrieveAnalAlgoRsp(SRetrieveAnalAlgoRsp* pRsp); +int32_t tSerializeRetrieveAnalyticAlgoReq(void* buf, int32_t bufLen, SRetrieveAnalyticsAlgoReq* pReq); +int32_t tDeserializeRetrieveAnalyticAlgoReq(void* buf, int32_t bufLen, SRetrieveAnalyticsAlgoReq* pReq); +int32_t tSerializeRetrieveAnalyticAlgoRsp(void* buf, int32_t bufLen, SRetrieveAnalyticAlgoRsp* pRsp); +int32_t tDeserializeRetrieveAnalyticAlgoRsp(void* buf, int32_t bufLen, SRetrieveAnalyticAlgoRsp* pRsp); +void tFreeRetrieveAnalyticAlgoRsp(SRetrieveAnalyticAlgoRsp* pRsp); typedef struct { int8_t alterType; diff --git a/source/common/src/msg/tmsg.c b/source/common/src/msg/tmsg.c index 5d99ef8fea..930408ef9b 100644 --- a/source/common/src/msg/tmsg.c +++ b/source/common/src/msg/tmsg.c @@ -2297,7 +2297,7 @@ _exit: return code; } -int32_t tSerializeRetrieveAnalAlgoReq(void *buf, int32_t bufLen, SRetrieveAnalAlgoReq *pReq) { +int32_t tSerializeRetrieveAnalyticAlgoReq(void *buf, int32_t bufLen, SRetrieveAnalyticsAlgoReq *pReq) { SEncoder encoder = {0}; int32_t code = 0; int32_t lino; @@ -2319,7 +2319,7 @@ _exit: return tlen; } -int32_t tDeserializeRetrieveAnalAlgoReq(void *buf, int32_t bufLen, SRetrieveAnalAlgoReq *pReq) { +int32_t tDeserializeRetrieveAnalyticAlgoReq(void *buf, int32_t bufLen, SRetrieveAnalyticsAlgoReq *pReq) { SDecoder decoder = {0}; int32_t code = 0; int32_t lino; @@ -2336,7 +2336,7 @@ _exit: return code; } -int32_t tSerializeRetrieveAnalAlgoRsp(void *buf, int32_t bufLen, SRetrieveAnalAlgoRsp *pRsp) { +int32_t tSerializeRetrieveAnalyticAlgoRsp(void *buf, int32_t bufLen, SRetrieveAnalyticAlgoRsp *pRsp) { SEncoder encoder = {0}; int32_t code = 0; int32_t lino; @@ -2387,7 +2387,7 @@ _exit: return tlen; } -int32_t tDeserializeRetrieveAnalAlgoRsp(void *buf, int32_t bufLen, SRetrieveAnalAlgoRsp *pRsp) { +int32_t tDeserializeRetrieveAnalyticAlgoRsp(void *buf, int32_t bufLen, SRetrieveAnalyticAlgoRsp *pRsp) { if (pRsp->hash == NULL) { pRsp->hash = taosHashInit(64, MurmurHash3_32, true, HASH_ENTRY_LOCK); if (pRsp->hash == NULL) { @@ -2425,7 +2425,10 @@ int32_t tDeserializeRetrieveAnalAlgoRsp(void *buf, int32_t bufLen, SRetrieveAnal TAOS_CHECK_EXIT(tDecodeBinaryAlloc(&decoder, (void **)&url.url, NULL) < 0); } - TAOS_CHECK_EXIT(taosHashPut(pRsp->hash, name, nameLen, &url, sizeof(SAnalyticsUrl))); + char dstName[TSDB_ANALYTIC_ALGO_NAME_LEN] = {0}; + strntolower(dstName, name, nameLen); + + TAOS_CHECK_EXIT(taosHashPut(pRsp->hash, dstName, nameLen, &url, sizeof(SAnalyticsUrl))); } tEndDecode(&decoder); @@ -2435,7 +2438,7 @@ _exit: return code; } -void tFreeRetrieveAnalAlgoRsp(SRetrieveAnalAlgoRsp *pRsp) { +void tFreeRetrieveAnalyticAlgoRsp(SRetrieveAnalyticAlgoRsp *pRsp) { void *pIter = taosHashIterate(pRsp->hash, NULL); while (pIter != NULL) { SAnalyticsUrl *pUrl = (SAnalyticsUrl *)pIter; diff --git a/source/dnode/mgmt/mgmt_dnode/src/dmHandle.c b/source/dnode/mgmt/mgmt_dnode/src/dmHandle.c index fc4ead8973..80f8a749ea 100644 --- a/source/dnode/mgmt/mgmt_dnode/src/dmHandle.c +++ b/source/dnode/mgmt/mgmt_dnode/src/dmHandle.c @@ -98,15 +98,15 @@ static void dmMayShouldUpdateAnalFunc(SDnodeMgmt *pMgmt, int64_t newVer) { if (oldVer == newVer) return; dDebug("analysis on dnode ver:%" PRId64 ", status ver:%" PRId64, oldVer, newVer); - SRetrieveAnalAlgoReq req = {.dnodeId = pMgmt->pData->dnodeId, .analVer = oldVer}; - int32_t contLen = tSerializeRetrieveAnalAlgoReq(NULL, 0, &req); + SRetrieveAnalyticsAlgoReq req = {.dnodeId = pMgmt->pData->dnodeId, .analVer = oldVer}; + int32_t contLen = tSerializeRetrieveAnalyticAlgoReq(NULL, 0, &req); if (contLen < 0) { dError("failed to serialize analysis function ver request since %s", tstrerror(contLen)); return; } void *pHead = rpcMallocCont(contLen); - contLen = tSerializeRetrieveAnalAlgoReq(pHead, contLen, &req); + contLen = tSerializeRetrieveAnalyticAlgoReq(pHead, contLen, &req); if (contLen < 0) { rpcFreeCont(pHead); dError("failed to serialize analysis function ver request since %s", tstrerror(contLen)); diff --git a/source/dnode/mgmt/node_mgmt/src/dmTransport.c b/source/dnode/mgmt/node_mgmt/src/dmTransport.c index 8ab14cff2f..d89e90bf90 100644 --- a/source/dnode/mgmt/node_mgmt/src/dmTransport.c +++ b/source/dnode/mgmt/node_mgmt/src/dmTransport.c @@ -116,13 +116,13 @@ static bool dmIsForbiddenIp(int8_t forbidden, char *user, uint32_t clientIp) { } } -static void dmUpdateAnalFunc(SDnodeData *pData, void *pTrans, SRpcMsg *pRpc) { - SRetrieveAnalAlgoRsp rsp = {0}; - if (tDeserializeRetrieveAnalAlgoRsp(pRpc->pCont, pRpc->contLen, &rsp) == 0) { +static void dmUpdateAnalyticFunc(SDnodeData *pData, void *pTrans, SRpcMsg *pRpc) { + SRetrieveAnalyticAlgoRsp rsp = {0}; + if (tDeserializeRetrieveAnalyticAlgoRsp(pRpc->pCont, pRpc->contLen, &rsp) == 0) { taosAnalyUpdate(rsp.ver, rsp.hash); rsp.hash = NULL; } - tFreeRetrieveAnalAlgoRsp(&rsp); + tFreeRetrieveAnalyticAlgoRsp(&rsp); rpcFreeCont(pRpc->pCont); } @@ -176,7 +176,7 @@ static void dmProcessRpcMsg(SDnode *pDnode, SRpcMsg *pRpc, SEpSet *pEpSet) { dmUpdateRpcIpWhite(&pDnode->data, pTrans->serverRpc, pRpc); return; case TDMT_MND_RETRIEVE_ANAL_ALGO_RSP: - dmUpdateAnalFunc(&pDnode->data, pTrans->serverRpc, pRpc); + dmUpdateAnalyticFunc(&pDnode->data, pTrans->serverRpc, pRpc); return; default: break; diff --git a/source/dnode/mnode/impl/src/mndAnode.c b/source/dnode/mnode/impl/src/mndAnode.c index 0777c2e247..163e697cc1 100644 --- a/source/dnode/mnode/impl/src/mndAnode.c +++ b/source/dnode/mnode/impl/src/mndAnode.c @@ -847,10 +847,10 @@ static int32_t mndProcessAnalAlgoReq(SRpcMsg *pReq) { SAnalyticsUrl url; int32_t nameLen; char name[TSDB_ANALYTIC_ALGO_KEY_LEN]; - SRetrieveAnalAlgoReq req = {0}; - SRetrieveAnalAlgoRsp rsp = {0}; + SRetrieveAnalyticsAlgoReq req = {0}; + SRetrieveAnalyticAlgoRsp rsp = {0}; - TAOS_CHECK_GOTO(tDeserializeRetrieveAnalAlgoReq(pReq->pCont, pReq->contLen, &req), NULL, _OVER); + TAOS_CHECK_GOTO(tDeserializeRetrieveAnalyticAlgoReq(pReq->pCont, pReq->contLen, &req), NULL, _OVER); rsp.ver = sdbGetTableVer(pSdb, SDB_ANODE); if (req.analVer != rsp.ver) { @@ -906,15 +906,15 @@ static int32_t mndProcessAnalAlgoReq(SRpcMsg *pReq) { } } - int32_t contLen = tSerializeRetrieveAnalAlgoRsp(NULL, 0, &rsp); + int32_t contLen = tSerializeRetrieveAnalyticAlgoRsp(NULL, 0, &rsp); void *pHead = rpcMallocCont(contLen); - (void)tSerializeRetrieveAnalAlgoRsp(pHead, contLen, &rsp); + (void)tSerializeRetrieveAnalyticAlgoRsp(pHead, contLen, &rsp); pReq->info.rspLen = contLen; pReq->info.rsp = pHead; _OVER: - tFreeRetrieveAnalAlgoRsp(&rsp); + tFreeRetrieveAnalyticAlgoRsp(&rsp); TAOS_RETURN(code); } diff --git a/source/libs/executor/src/forecastoperator.c b/source/libs/executor/src/forecastoperator.c index 25052af523..ad7f37cad9 100644 --- a/source/libs/executor/src/forecastoperator.c +++ b/source/libs/executor/src/forecastoperator.c @@ -145,6 +145,7 @@ static int32_t forecastCloseBuf(SForecastSupp* pSupp, const char* id) { if (!hasWncheck) { qDebug("%s forecast wncheck not found from %s, use default:%" PRId64, id, pSupp->algoOpt, wncheck); } + code = taosAnalyBufWriteOptInt(pBuf, "wncheck", wncheck); if (code != 0) return code; diff --git a/tools/tdgpt/taosanalytics/algo/fc/arima.py b/tools/tdgpt/taosanalytics/algo/fc/arima.py index 787cb757df..79d7136440 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/arima.py +++ b/tools/tdgpt/taosanalytics/algo/fc/arima.py @@ -86,11 +86,11 @@ class _ArimaService(AbstractForecastService): if len(self.list) > 3000: raise ValueError("number of input data is too large") - if self.fc_rows <= 0: + if self.rows <= 0: raise ValueError("fc rows is not specified yet") - res, mse, model_info = self.__do_forecast_helper(self.fc_rows) - insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) + res, mse, model_info = self.__do_forecast_helper(self.rows) + insert_ts_list(res, self.start_ts, self.time_step, self.rows) return { "mse": mse, diff --git a/tools/tdgpt/taosanalytics/algo/fc/gpt.py b/tools/tdgpt/taosanalytics/algo/fc/gpt.py index 6a6e13edb2..65fa0240c1 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/gpt.py +++ b/tools/tdgpt/taosanalytics/algo/fc/gpt.py @@ -10,14 +10,14 @@ from taosanalytics.service import AbstractForecastService class _GPTService(AbstractForecastService): - name = 'TDtsfm_1' + name = 'tdtsfm_1' desc = "internal gpt forecast model based on transformer" def __init__(self): super().__init__() self.table_name = None - self.service_host = 'http://127.0.0.1:5000/ds_predict' + self.service_host = 'http://127.0.0.1:5000/tdtsfm' self.headers = {'Content-Type': 'application/json'} self.std = None @@ -29,11 +29,11 @@ class _GPTService(AbstractForecastService): if self.list is None or len(self.list) < self.period: raise ValueError("number of input data is less than the periods") - if self.fc_rows <= 0: + if self.rows <= 0: raise ValueError("fc rows is not specified yet") # let's request the gpt service - data = {"input": self.list, 'next_len': self.fc_rows} + data = {"input": self.list, 'next_len': self.rows} try: response = requests.post(self.service_host, data=json.dumps(data), headers=self.headers) except Exception as e: @@ -53,7 +53,7 @@ class _GPTService(AbstractForecastService): "res": [pred_y] } - insert_ts_list(res["res"], self.start_ts, self.time_step, self.fc_rows) + insert_ts_list(res["res"], self.start_ts, self.time_step, self.rows) return res diff --git a/tools/tdgpt/taosanalytics/algo/fc/holtwinters.py b/tools/tdgpt/taosanalytics/algo/fc/holtwinters.py index d8225eaa5a..24aea44fdb 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/holtwinters.py +++ b/tools/tdgpt/taosanalytics/algo/fc/holtwinters.py @@ -66,11 +66,11 @@ class _HoltWintersService(AbstractForecastService): if self.list is None or len(self.list) < self.period: raise ValueError("number of input data is less than the periods") - if self.fc_rows <= 0: + if self.rows <= 0: raise ValueError("fc rows is not specified yet") - res, mse = self.__do_forecast_helper(self.list, self.fc_rows) - insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) + res, mse = self.__do_forecast_helper(self.list, self.rows) + insert_ts_list(res, self.start_ts, self.time_step, self.rows) # add the conf range if required return { diff --git a/tools/tdgpt/taosanalytics/algo/fc/lstm.py b/tools/tdgpt/taosanalytics/algo/fc/lstm.py index 5edae7fc9f..72534ab6ab 100644 --- a/tools/tdgpt/taosanalytics/algo/fc/lstm.py +++ b/tools/tdgpt/taosanalytics/algo/fc/lstm.py @@ -46,7 +46,7 @@ class _LSTMService(AbstractForecastService): res = self.model.predict(self.list) - insert_ts_list(res, self.start_ts, self.time_step, self.fc_rows) + insert_ts_list(res, self.start_ts, self.time_step, self.rows) if self.return_conf: res1 = [res.tolist(), res.tolist(), res.tolist()], None diff --git a/tools/tdgpt/taosanalytics/algo/forecast.py b/tools/tdgpt/taosanalytics/algo/forecast.py index 4baa92fe15..5c681bf3b3 100644 --- a/tools/tdgpt/taosanalytics/algo/forecast.py +++ b/tools/tdgpt/taosanalytics/algo/forecast.py @@ -41,7 +41,7 @@ def do_forecast(input_list, ts_list, algo_name, params): def do_add_fc_params(params, json_obj): """ add params into parameters """ if "forecast_rows" in json_obj: - params["fc_rows"] = int(json_obj["forecast_rows"]) + params["rows"] = int(json_obj["forecast_rows"]) if "start" in json_obj: params["start_ts"] = int(json_obj["start"]) diff --git a/tools/tdgpt/taosanalytics/app.py b/tools/tdgpt/taosanalytics/app.py index cff5f74447..e2196062fb 100644 --- a/tools/tdgpt/taosanalytics/app.py +++ b/tools/tdgpt/taosanalytics/app.py @@ -147,7 +147,7 @@ def handle_forecast_req(): try: res1 = do_forecast(payload[data_index], payload[ts_index], algo, params) - res = {"option": options, "rows": params["fc_rows"]} + res = {"option": options, "rows": params["rows"]} res.update(res1) app_logger.log_inst.debug("forecast result: %s", res) diff --git a/tools/tdgpt/taosanalytics/service.py b/tools/tdgpt/taosanalytics/service.py index b79d21501a..9e960f3e58 100644 --- a/tools/tdgpt/taosanalytics/service.py +++ b/tools/tdgpt/taosanalytics/service.py @@ -77,14 +77,14 @@ class AbstractForecastService(AbstractAnalyticsService, ABC): self.period = 0 self.start_ts = 0 self.time_step = 0 - self.fc_rows = 0 + self.rows = 0 self.return_conf = 1 self.conf = 0.05 def set_params(self, params: dict) -> None: - if not {'start_ts', 'time_step', 'fc_rows'}.issubset(params.keys()): - raise ValueError('params are missing, start_ts, time_step, fc_rows are all required') + if not {'start_ts', 'time_step', 'rows'}.issubset(params.keys()): + raise ValueError('params are missing, start_ts, time_step, rows are all required') self.start_ts = int(params['start_ts']) @@ -93,9 +93,9 @@ class AbstractForecastService(AbstractAnalyticsService, ABC): if self.time_step <= 0: raise ValueError('time_step should be greater than 0') - self.fc_rows = int(params['fc_rows']) + self.rows = int(params['rows']) - if self.fc_rows <= 0: + if self.rows <= 0: raise ValueError('fc rows is not specified yet') self.period = int(params['period']) if 'period' in params else 0 @@ -113,5 +113,5 @@ class AbstractForecastService(AbstractAnalyticsService, ABC): def get_params(self): return { "period": self.period, "start": self.start_ts, "every": self.time_step, - "forecast_rows": self.fc_rows, "return_conf": self.return_conf, "conf": self.conf + "forecast_rows": self.rows, "return_conf": self.return_conf, "conf": self.conf } diff --git a/tools/tdgpt/taosanalytics/test/forecast_test.py b/tools/tdgpt/taosanalytics/test/forecast_test.py index 4b2368c6ba..9e417d9263 100644 --- a/tools/tdgpt/taosanalytics/test/forecast_test.py +++ b/tools/tdgpt/taosanalytics/test/forecast_test.py @@ -41,7 +41,7 @@ class ForecastTest(unittest.TestCase): s.set_input_list(data, ts) self.assertRaises(ValueError, s.execute) - s.set_params({"fc_rows": 10, "start_ts": 171000000, "time_step": 86400 * 30}) + s.set_params({"rows": 10, "start_ts": 171000000, "time_step": 86400 * 30}) r = s.execute() draw_fc_results(data, len(r["res"]) > 2, r["res"], len(r["res"][0]), "holtwinters") @@ -54,7 +54,7 @@ class ForecastTest(unittest.TestCase): s.set_input_list(data, ts) s.set_params( { - "fc_rows": 10, "trend": 'mul', "seasonal": 'mul', "start_ts": 171000000, + "rows": 10, "trend": 'mul', "seasonal": 'mul', "start_ts": 171000000, "time_step": 86400 * 30, "period": 12 } ) @@ -71,28 +71,28 @@ class ForecastTest(unittest.TestCase): self.assertRaises(ValueError, s.set_params, {"trend": "mul"}) - self.assertRaises(ValueError, s.set_params, {"trend": "mul", "fc_rows": 10}) + self.assertRaises(ValueError, s.set_params, {"trend": "mul", "rows": 10}) self.assertRaises(ValueError, s.set_params, {"trend": "multi"}) self.assertRaises(ValueError, s.set_params, {"seasonal": "additive"}) self.assertRaises(ValueError, s.set_params, { - "fc_rows": 10, "trend": 'multi', "seasonal": 'addi', "start_ts": 171000000, + "rows": 10, "trend": 'multi', "seasonal": 'addi', "start_ts": 171000000, "time_step": 86400 * 30, "period": 12} ) self.assertRaises(ValueError, s.set_params, - {"fc_rows": 10, "trend": 'mul', "seasonal": 'add', "time_step": 86400 * 30, "period": 12} + {"rows": 10, "trend": 'mul', "seasonal": 'add', "time_step": 86400 * 30, "period": 12} ) - s.set_params({"fc_rows": 10, "start_ts": 171000000, "time_step": 86400 * 30}) + s.set_params({"rows": 10, "start_ts": 171000000, "time_step": 86400 * 30}) - self.assertRaises(ValueError, s.set_params, {"fc_rows": 'abc', "start_ts": 171000000, "time_step": 86400 * 30}) + self.assertRaises(ValueError, s.set_params, {"rows": 'abc', "start_ts": 171000000, "time_step": 86400 * 30}) - self.assertRaises(ValueError, s.set_params, {"fc_rows": 10, "start_ts": "aaa", "time_step": "30"}) + self.assertRaises(ValueError, s.set_params, {"rows": 10, "start_ts": "aaa", "time_step": "30"}) - self.assertRaises(ValueError, s.set_params, {"fc_rows": 10, "start_ts": 171000000, "time_step": 0}) + self.assertRaises(ValueError, s.set_params, {"rows": 10, "start_ts": 171000000, "time_step": 0}) def test_arima(self): """arima algorithm check""" @@ -103,7 +103,7 @@ class ForecastTest(unittest.TestCase): self.assertRaises(ValueError, s.execute) s.set_params( - {"fc_rows": 10, "start_ts": 171000000, "time_step": 86400 * 30, "period": 12, + {"rows": 10, "start_ts": 171000000, "time_step": 86400 * 30, "period": 12, "start_p": 0, "max_p": 10, "start_q": 0, "max_q": 10} ) r = s.execute() @@ -120,7 +120,7 @@ class ForecastTest(unittest.TestCase): # s = loader.get_service("td_gpt_fc") # s.set_input_list(data, ts) # - # s.set_params({"host":'192.168.2.90:5000/ds_predict', 'fc_rows': 10, 'start_ts': 171000000, 'time_step': 86400*30}) + # s.set_params({"host":'192.168.2.90:5000/ds_predict', 'rows': 10, 'start_ts': 171000000, 'time_step': 86400*30}) # r = s.execute() # # rows = len(r["res"][0]) From 8cfb45d25757cd94fa7805d62438c76021d35a60 Mon Sep 17 00:00:00 2001 From: Mario Peng <48949600+Pengrongkun@users.noreply.github.com> Date: Sun, 23 Mar 2025 16:40:05 +0800 Subject: [PATCH 37/38] fix: insert into ntb get fields problem (#30370) --- source/client/test/stmt2Test.cpp | 28 ++++++++++++++++++++++++++ source/libs/parser/src/parInsertStmt.c | 7 ++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/source/client/test/stmt2Test.cpp b/source/client/test/stmt2Test.cpp index 5e50760648..4545c692aa 100644 --- a/source/client/test/stmt2Test.cpp +++ b/source/client/test/stmt2Test.cpp @@ -1213,6 +1213,34 @@ TEST(stmt2Case, stmt2_insert_non_statndard) { taos_stmt2_close(stmt); } + // get fields insert into ? valuse + { + TAOS_STMT2* stmt = taos_stmt2_init(taos, &option); + ASSERT_NE(stmt, nullptr); + do_query(taos, "create table stmt2_testdb_6.ntb(ts timestamp, b binary(10))"); + do_query(taos, "use stmt2_testdb_6"); + const char* sql = "INSERT INTO ? VALUES (?,?)"; + printf("stmt2 [%s] : %s\n", "get fields", sql); + int code = taos_stmt2_prepare(stmt, sql, 0); + checkError(stmt, code); + + char* tbname = "ntb"; + TAOS_STMT2_BINDV bindv = {1, &tbname, NULL, NULL}; + code = taos_stmt2_bind_param(stmt, &bindv, -1); + ASSERT_EQ(code, 0); + + int fieldNum = 0; + TAOS_FIELD_ALL* pFields = NULL; + code = taos_stmt2_get_fields(stmt, &fieldNum, &pFields); + checkError(stmt, code); + ASSERT_EQ(fieldNum, 3); + ASSERT_STREQ(pFields[0].name, "tbname"); + ASSERT_STREQ(pFields[1].name, "ts"); + ASSERT_STREQ(pFields[2].name, "b"); + + taos_stmt2_close(stmt); + } + do_query(taos, "drop database if exists stmt2_testdb_6"); taos_close(taos); } diff --git a/source/libs/parser/src/parInsertStmt.c b/source/libs/parser/src/parInsertStmt.c index 2dc9e96264..ecf566c925 100644 --- a/source/libs/parser/src/parInsertStmt.c +++ b/source/libs/parser/src/parInsertStmt.c @@ -1073,8 +1073,9 @@ int32_t buildStbBoundFields(SBoundColInfo boundColsInfo, SSchema* pSchema, int32 STableMeta* pMeta, void* boundTags, uint8_t tbNameFlag) { SBoundColInfo* tags = (SBoundColInfo*)boundTags; bool hastag = (tags != NULL) && !(tbNameFlag & IS_FIXED_TAG); - int32_t numOfBound = - boundColsInfo.numOfBound + ((tbNameFlag & IS_FIXED_VALUE) == 0 && (tbNameFlag & USING_CLAUSE) != 0 ? 1 : 0); + bool hasPreBindTbname = + (tbNameFlag & IS_FIXED_VALUE) == 0 && ((tbNameFlag & USING_CLAUSE) != 0 || pMeta->tableType == TSDB_NORMAL_TABLE); + int32_t numOfBound = boundColsInfo.numOfBound + (hasPreBindTbname ? 1 : 0); if (hastag) { numOfBound += tags->mixTagsCols ? 0 : tags->numOfBound; } @@ -1085,7 +1086,7 @@ int32_t buildStbBoundFields(SBoundColInfo boundColsInfo, SSchema* pSchema, int32 return terrno; } - if ((tbNameFlag & IS_FIXED_VALUE) == 0 && (tbNameFlag & USING_CLAUSE) != 0) { + if (hasPreBindTbname) { (*fields)[idx].field_type = TAOS_FIELD_TBNAME; tstrncpy((*fields)[idx].name, "tbname", sizeof((*fields)[idx].name)); (*fields)[idx].type = TSDB_DATA_TYPE_BINARY; From 12b3ec6fa503435844c837854dbafec205cb106b Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Sun, 23 Mar 2025 17:03:20 +0800 Subject: [PATCH 38/38] fix(gpt): fix the bug for algo name case sensitive (#30369) * fix(gpt): fix the bug for algo name case sensitive * refactor: format * refactor(stream): fix error. --- source/libs/stream/src/streamStartTask.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/source/libs/stream/src/streamStartTask.c b/source/libs/stream/src/streamStartTask.c index 13cf4a41cc..de782c0902 100644 --- a/source/libs/stream/src/streamStartTask.c +++ b/source/libs/stream/src/streamStartTask.c @@ -158,9 +158,17 @@ int32_t streamMetaStartAllTasks(SStreamMeta* pMeta) { pMeta->startInfo.curStage = START_MARK_REQ_CHKPID; SStartTaskStageInfo info = {.stage = pMeta->startInfo.curStage, .ts = now}; - taosArrayPush(pMeta->startInfo.pStagesList, &info); - stDebug("vgId:%d %d task(s) 0 stage -> mark_req stage, reqTs:%" PRId64 " numOfStageHist:%d", pMeta->vgId, numOfConsensusChkptIdTasks, - info.ts, (int32_t)taosArrayGetSize(pMeta->startInfo.pStagesList)); + void* p = taosArrayPush(pMeta->startInfo.pStagesList, &info); + int32_t num = (int32_t)taosArrayGetSize(pMeta->startInfo.pStagesList); + + if (p != NULL) { + stDebug("vgId:%d %d task(s) 0 stage -> mark_req stage, reqTs:%" PRId64 " numOfStageHist:%d", pMeta->vgId, + numOfConsensusChkptIdTasks, info.ts, num); + } else { + stError("vgId:%d %d task(s) 0 stage -> mark_req stage, reqTs:%" PRId64 + " numOfStageHist:%d, FAILED, out of memory", + pMeta->vgId, numOfConsensusChkptIdTasks, info.ts, num); + } } // prepare the fill-history task before starting all stream tasks, to avoid fill-history tasks are started without @@ -230,8 +238,8 @@ static void streamMetaLogLaunchTasksInfo(SStreamMeta* pMeta, int32_t numOfTotal, displayStatusInfo(pMeta, pStartInfo->pFailedTaskSet, false); } -int32_t streamMetaAddTaskLaunchResultNoLock(SStreamMeta* pMeta, int64_t streamId, int32_t taskId, - int64_t startTs, int64_t endTs, bool ready) { +int32_t streamMetaAddTaskLaunchResultNoLock(SStreamMeta* pMeta, int64_t streamId, int32_t taskId, int64_t startTs, + int64_t endTs, bool ready) { STaskStartInfo* pStartInfo = &pMeta->startInfo; STaskId id = {.streamId = streamId, .taskId = taskId}; int32_t vgId = pMeta->vgId; @@ -312,7 +320,7 @@ bool allCheckDownstreamRsp(SStreamMeta* pMeta, STaskStartInfo* pStartInfo, int32 if (px == NULL) { px = taosHashGet(pStartInfo->pFailedTaskSet, &idx, sizeof(idx)); if (px == NULL) { - stDebug("vgId:%d s-task:0x%x start result not rsp yet", pMeta->vgId, (int32_t) idx.taskId); + stDebug("vgId:%d s-task:0x%x start result not rsp yet", pMeta->vgId, (int32_t)idx.taskId); return false; } }