提交 eba73278 编写于 作者: H Haojun Liao

[td-10564] add more code for query module.

上级 04c53c29
......@@ -36,6 +36,8 @@ typedef struct SVariant {
};
} SVariant;
int32_t toInteger(const char* z, int32_t n, int32_t base, int64_t* value, bool* issigned);
bool taosVariantIsValid(SVariant *pVar);
void taosVariantCreate(SVariant *pVar, char* z, int32_t n, int32_t type);
......
......@@ -10,4 +10,6 @@ target_link_libraries(
PUBLIC os
PUBLIC util
INTERFACE api
)
\ No newline at end of file
)
ADD_SUBDIRECTORY(test)
......@@ -15,8 +15,8 @@
#include "os.h"
#include "taos.h"
#include "thash.h"
#include "taosdef.h"
#include "thash.h"
#include "ttime.h"
#include "ttokendef.h"
#include "ttypes.h"
......@@ -39,6 +39,42 @@
assert(0); \
} while (0)
int32_t toInteger(const char* z, int32_t n, int32_t base, int64_t* value, bool* isSigned) {
errno = 0;
char* endPtr = NULL;
int32_t index = 0;
bool specifiedSign = (z[0] == '+' || z[0] == '-');
if (specifiedSign) {
*isSigned = true;
index = 1;
}
uint64_t val = strtoull(&z[index], &endPtr, base);
if (errno == ERANGE || errno == EINVAL) {
errno = 0;
return -1;
}
if (specifiedSign && val > INT64_MAX) {
return -1;
}
if (endPtr - &z[index] != n - index) {
return -1;
}
*isSigned = specifiedSign || (val <= INT64_MAX);
if (*isSigned) {
*value = (z[0] == '-')? -val:val;
} else {
*(uint64_t*) value = val;
}
return 0;
}
void taosVariantCreate(SVariant *pVar, char* z, int32_t n, int32_t type) {
int32_t ret = 0;
memset(pVar, 0, sizeof(SVariant));
......@@ -52,7 +88,6 @@ void taosVariantCreate(SVariant *pVar, char* z, int32_t n, int32_t type) {
} else {
return;
}
break;
}
......@@ -60,38 +95,38 @@ void taosVariantCreate(SVariant *pVar, char* z, int32_t n, int32_t type) {
case TSDB_DATA_TYPE_SMALLINT:
case TSDB_DATA_TYPE_BIGINT:
case TSDB_DATA_TYPE_INT:{
// ret = tStrToInteger(token->z, token->type, token->n, &pVar->i64, true);
// if (ret != 0) {
// SToken t = {0};
// tGetToken(token->z, &t.type);
// if (t.type == TK_MINUS) { // it is a signed number which is greater than INT64_MAX or less than INT64_MIN
// pVar->nType = -1; // -1 means error type
// return;
// }
//
// // data overflow, try unsigned parse the input number
// ret = tStrToInteger(token->z, token->type, token->n, &pVar->i64, false);
// if (ret != 0) {
// pVar->nType = -1; // -1 means error type
// return;
// }
// }
bool sign = true;
int32_t base = 10;
if (type == TK_HEX) {
base = 16;
} else if (type == TK_OCT) {
base = 8;
} else if (type == TK_BIN) {
base = 2;
}
ret = toInteger(z, n, base, &pVar->i64, &sign);
if (ret != 0) {
pVar->nType = -1; // -1 means error type
return;
}
pVar->nType = (sign)? TSDB_DATA_TYPE_BIGINT:TSDB_DATA_TYPE_UBIGINT;
break;
}
case TSDB_DATA_TYPE_DOUBLE:
case TSDB_DATA_TYPE_FLOAT: {
pVar->d = strtod(z, NULL);
break;
}
case TSDB_DATA_TYPE_BINARY: {
pVar->pz = strndup(z, n);
pVar->nLen = strRmquote(pVar->pz, n);
break;
}
case TSDB_DATA_TYPE_TIMESTAMP: {
assert(0);
pVar->i64 = taosGetTimestamp(TSDB_TIME_PRECISION_NANO);
break;
}
......
MESSAGE(STATUS "build parser unit test")
# GoogleTest requires at least C++11
SET(CMAKE_CXX_STANDARD 11)
AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR} SOURCE_LIST)
ADD_EXECUTABLE(commonTest ${SOURCE_LIST})
TARGET_LINK_LIBRARIES(
commonTest
PUBLIC os util common gtest
)
TARGET_INCLUDE_DIRECTORIES(
commonTest
PUBLIC "${CMAKE_SOURCE_DIR}/include/libs/common/"
PRIVATE "${CMAKE_SOURCE_DIR}/source/libs/common/inc"
)
#include <gtest/gtest.h>
#include <iostream>
#pragma GCC diagnostic ignored "-Wwrite-strings"
#pragma GCC diagnostic ignored "-Wunused-function"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wsign-compare"
#include "os.h"
#include "taos.h"
#include "tvariant.h"
#include "tdef.h"
namespace {
//
} // namespace
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
TEST(testCase, toInteger_test) {
char* s = "123";
uint32_t type = 0;
int64_t val = 0;
bool sign = true;
int32_t ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 123);
ASSERT_EQ(sign, true);
s = "9223372036854775807";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 9223372036854775807);
ASSERT_EQ(sign, true);
s = "9323372036854775807";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 9323372036854775807u);
ASSERT_EQ(sign, false);
s = "-9323372036854775807";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, -1);
s = "-1";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, -1);
ASSERT_EQ(sign, true);
s = "-9223372036854775807";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, -9223372036854775807);
ASSERT_EQ(sign, true);
s = "1000u";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, -1);
s = "0x10";
ret = toInteger(s, strlen(s), 16, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 16);
ASSERT_EQ(sign, true);
s = "110";
ret = toInteger(s, strlen(s), 2, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 6);
ASSERT_EQ(sign, true);
s = "110";
ret = toInteger(s, strlen(s), 8, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 72);
ASSERT_EQ(sign, true);
//18446744073709551615 UINT64_MAX
s = "18446744073709551615";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, 0);
ASSERT_EQ(val, 18446744073709551615u);
ASSERT_EQ(sign, false);
s = "18446744073709551616";
ret = toInteger(s, strlen(s), 10, &val, &sign);
ASSERT_EQ(ret, -1);
}
......@@ -277,6 +277,7 @@ bool tSqlExprIsParentOfLeaf(tSqlExpr *pExpr);
void tSqlExprDestroy(tSqlExpr *pExpr);
SArray * tSqlExprListAppend(SArray *pList, tSqlExpr *pNode, SToken *pDistinct, SToken *pToken);
void tSqlExprListDestroy(SArray *pList);
void tSqlExprEvaluate(tSqlExpr* pExpr);
SSqlNode *tSetQuerySqlNode(SToken *pSelectToken, SArray *pSelNodeList, SRelationInfo *pFrom, tSqlExpr *pWhere,
SArray *pGroupby, SArray *pSortOrder, SIntervalVal *pInterval, SSessionWindowVal *ps,
......
......@@ -46,6 +46,16 @@ typedef struct SInsertStmtInfo {
*/
int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pSqlInfo, SQueryStmtInfo* pQueryInfo, int64_t id, char* msg, int32_t msgLen);
/**
*
* @param pNode
* @param tsPrecision
* @param msg
* @param msgBufLen
* @return
*/
int32_t evaluateSqlNode(SSqlNode* pNode, int32_t tsPrecision, char* msg, int32_t msgBufLen);
/**
*
* @param pSqlNode
......
......@@ -23,7 +23,8 @@ extern "C" {
#include "os.h"
#include "ttoken.h"
int32_t parserValidateNameToken(SToken* pToken);
int32_t parserValidateIdToken(SToken* pToken);
int32_t parserSetInvalidOperatorMsg(char* dst, int32_t dstBufLen, const char* msg);
#ifdef __cplusplus
}
......
......@@ -18,52 +18,6 @@
#include "astGenerator.h"
#include "tmsgtype.h"
int32_t tStrToInteger(const char* z, int16_t type, int32_t n, int64_t* value, bool issigned) {
errno = 0;
int32_t ret = 0;
char* endPtr = NULL;
if (type == TK_FLOAT) {
double v = strtod(z, &endPtr);
if ((errno == ERANGE && v == HUGE_VALF) || isinf(v) || isnan(v)) {
ret = -1;
} else if ((issigned && (v < INT64_MIN || v > INT64_MAX)) || ((!issigned) && (v < 0 || v > UINT64_MAX))) {
ret = -1;
} else {
*value = (int64_t) round(v);
}
errno = 0;
return ret;
}
int32_t radix = 10;
if (type == TK_HEX) {
radix = 16;
} else if (type == TK_BIN) {
radix = 2;
}
// the string may be overflow according to errno
if (!issigned) {
const char *p = z;
while(*p != 0 && *p == ' ') p++;
if (*p != 0 && *p == '-') { return -1;}
*value = strtoull(z, &endPtr, radix);
} else {
*value = strtoll(z, &endPtr, radix);
}
// not a valid integer number, return error
if (endPtr - z != n || errno == ERANGE) {
ret = -1;
}
errno = 0;
return ret;
}
SArray *tListItemAppend(SArray *pList, SVariant *pVar, uint8_t sortOrder) {
if (pList == NULL) {
pList = taosArrayInit(4, sizeof(SListItem));
......@@ -173,7 +127,6 @@ SRelationInfo *addSubquery(SRelationInfo *pRelationInfo, SArray *pSub, SToken *p
}
// sql expr leaf node
// todo Evalute the value during the validation process of AST.
tSqlExpr *tSqlExprCreateIdValue(SToken *pToken, int32_t optrType) {
tSqlExpr *pSqlExpr = calloc(1, sizeof(tSqlExpr));
......@@ -189,34 +142,10 @@ tSqlExpr *tSqlExprCreateIdValue(SToken *pToken, int32_t optrType) {
pSqlExpr->tokenId = optrType;
pSqlExpr->type = SQL_NODE_VALUE;
} else if (optrType == TK_INTEGER || optrType == TK_STRING || optrType == TK_FLOAT || optrType == TK_BOOL) {
// if (pToken) {
// toTSDBType(pToken->type);
// tVariantCreate(&pSqlExpr->value, pToken);
// }
pSqlExpr->tokenId = optrType;
pSqlExpr->type = SQL_NODE_VALUE;
} else if (optrType == TK_NOW) {
// use nanosecond by default TODO set value after getting database precision
// pSqlExpr->value.i64 = taosGetTimestamp(TSDB_TIME_PRECISION_NANO);
// pSqlExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pSqlExpr->tokenId = TK_TIMESTAMP; // TK_TIMESTAMP used to denote the time value is in microsecond
pSqlExpr->type = SQL_NODE_VALUE;
// pSqlExpr->flags |= 1 << EXPR_FLAG_NS_TIMESTAMP;
} else if (optrType == TK_VARIABLE) {
// use nanosecond by default
// TODO set value after getting database precision
// if (pToken) {
// char unit = 0;
// int32_t ret = parseAbsoluteDuration(pToken->z, pToken->n, &pSqlExpr->value.i64, &unit, TSDB_TIME_PRECISION_NANO);
// if (ret != TSDB_CODE_SUCCESS) {
// terrno = TSDB_CODE_TSC_SQL_SYNTAX_ERROR;
// }
// }
// pSqlExpr->flags |= 1 << EXPR_FLAG_NS_TIMESTAMP;
// pSqlExpr->flags |= 1 << EXPR_FLAG_TIMESTAMP_VAR;
// pSqlExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pSqlExpr->tokenId = TK_TIMESTAMP;
} else if (optrType == TK_NOW || optrType == TK_VARIABLE) {
pSqlExpr->tokenId = optrType; // TK_TIMESTAMP used to denote this is a timestamp value
pSqlExpr->type = SQL_NODE_VALUE;
} else {
// Here it must be the column name (tk_id) if it is not a number or string.
......@@ -269,87 +198,7 @@ tSqlExpr *tSqlExprCreate(tSqlExpr *pLeft, tSqlExpr *pRight, int32_t optrType) {
pExpr->exprToken.type = pLeft->exprToken.type;
}
if ((pLeft != NULL && pRight != NULL) &&
(optrType == TK_PLUS || optrType == TK_MINUS || optrType == TK_STAR || optrType == TK_DIVIDE || optrType == TK_REM)) {
/*
* if a exprToken is noted as the TK_TIMESTAMP, the time precision is microsecond
* Otherwise, the time precision is adaptive, determined by the time precision from databases.
*/
if ((pLeft->tokenId == TK_INTEGER && pRight->tokenId == TK_INTEGER) ||
(pLeft->tokenId == TK_TIMESTAMP && pRight->tokenId == TK_TIMESTAMP)) {
pExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pExpr->tokenId = pLeft->tokenId;
pExpr->type = SQL_NODE_VALUE;
switch (optrType) {
case TK_PLUS: {
pExpr->value.i64 = pLeft->value.i64 + pRight->value.i64;
break;
}
case TK_MINUS: {
pExpr->value.i64 = pLeft->value.i64 - pRight->value.i64;
break;
}
case TK_STAR: {
pExpr->value.i64 = pLeft->value.i64 * pRight->value.i64;
break;
}
case TK_DIVIDE: {
pExpr->tokenId = TK_FLOAT;
pExpr->value.nType = TSDB_DATA_TYPE_DOUBLE;
pExpr->value.d = (double)pLeft->value.i64 / pRight->value.i64;
break;
}
case TK_REM: {
pExpr->value.i64 = pLeft->value.i64 % pRight->value.i64;
break;
}
}
tSqlExprDestroy(pLeft);
tSqlExprDestroy(pRight);
} else if ((pLeft->tokenId == TK_FLOAT && pRight->tokenId == TK_INTEGER) ||
(pLeft->tokenId == TK_INTEGER && pRight->tokenId == TK_FLOAT) ||
(pLeft->tokenId == TK_FLOAT && pRight->tokenId == TK_FLOAT)) {
pExpr->value.nType = TSDB_DATA_TYPE_DOUBLE;
pExpr->tokenId = TK_FLOAT;
pExpr->type = SQL_NODE_VALUE;
double left = (pLeft->value.nType == TSDB_DATA_TYPE_DOUBLE) ? pLeft->value.d : pLeft->value.i64;
double right = (pRight->value.nType == TSDB_DATA_TYPE_DOUBLE) ? pRight->value.d : pRight->value.i64;
switch (optrType) {
case TK_PLUS: {
pExpr->value.d = left + right;
break;
}
case TK_MINUS: {
pExpr->value.d = left - right;
break;
}
case TK_STAR: {
pExpr->value.d = left * right;
break;
}
case TK_DIVIDE: {
pExpr->value.d = left / right;
break;
}
case TK_REM: {
pExpr->value.d = left - ((int64_t)(left / right)) * right;
break;
}
}
tSqlExprDestroy(pLeft);
tSqlExprDestroy(pRight);
} else {
pExpr->tokenId = optrType;
pExpr->pLeft = pLeft;
pExpr->pRight = pRight;
}
} else if (optrType == TK_IN) {
if (optrType == TK_IN) {
pExpr->tokenId = optrType;
pExpr->pLeft = pLeft;
......@@ -501,6 +350,105 @@ void tSqlExprListDestroy(SArray *pList) {
taosArrayDestroyEx(pList, freeExprElem);
}
void tSqlExprEvaluate(tSqlExpr* pExpr) {
tSqlExpr *pLeft = pExpr->pLeft;
tSqlExpr *pRight = pExpr->pRight;
if (pLeft == NULL || pRight == NULL) {
return;
}
int32_t optrType = pExpr->tokenId;
if ((optrType == TK_PLUS || optrType == TK_MINUS || optrType == TK_STAR || optrType == TK_DIVIDE ||
optrType == TK_REM)) {
/*
* if a exprToken is noted as the TK_TIMESTAMP, the time precision is microsecond
* Otherwise, the time precision is adaptive, determined by the time precision from databases.
*/
int32_t ltoken = pLeft->tokenId;
int32_t rtoken = pRight->tokenId;
if ((ltoken == TK_INTEGER && rtoken == TK_INTEGER) || (ltoken == TK_TIMESTAMP && rtoken == TK_TIMESTAMP)) {
pExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pExpr->tokenId = ltoken;
pExpr->type = SQL_NODE_VALUE;
switch (optrType) {
case TK_PLUS: {
pExpr->value.i64 = pLeft->value.i64 + pRight->value.i64;
break;
}
case TK_MINUS: {
pExpr->value.i64 = pLeft->value.i64 - pRight->value.i64;
break;
}
case TK_STAR: {
pExpr->value.i64 = pLeft->value.i64 * pRight->value.i64;
break;
}
case TK_DIVIDE: {
pExpr->tokenId = TK_FLOAT;
pExpr->value.nType = TSDB_DATA_TYPE_DOUBLE;
pExpr->value.d = (double)pLeft->value.i64 / pRight->value.i64;
break;
}
case TK_REM: {
pExpr->value.i64 = pLeft->value.i64 % pRight->value.i64;
break;
}
default:
assert(0);
}
tSqlExprDestroy(pLeft);
tSqlExprDestroy(pRight);
pExpr->pLeft = NULL;
pExpr->pRight = NULL;
} else if ((ltoken == TK_FLOAT && rtoken == TK_INTEGER) || (ltoken == TK_INTEGER && rtoken == TK_FLOAT) ||
(ltoken == TK_FLOAT && rtoken == TK_FLOAT)) {
pExpr->value.nType = TSDB_DATA_TYPE_DOUBLE;
pExpr->tokenId = TK_FLOAT;
pExpr->type = SQL_NODE_VALUE;
double left = (pLeft->value.nType == TSDB_DATA_TYPE_DOUBLE) ? pLeft->value.d : pLeft->value.i64;
double right = (pRight->value.nType == TSDB_DATA_TYPE_DOUBLE) ? pRight->value.d : pRight->value.i64;
switch (optrType) {
case TK_PLUS: {
pExpr->value.d = left + right;
break;
}
case TK_MINUS: {
pExpr->value.d = left - right;
break;
}
case TK_STAR: {
pExpr->value.d = left * right;
break;
}
case TK_DIVIDE: {
pExpr->value.d = left / right;
break;
}
case TK_REM: {
pExpr->value.d = left - ((int64_t)(left / right)) * right;
break;
}
default:
assert(0);
}
tSqlExprDestroy(pLeft);
tSqlExprDestroy(pRight);
pExpr->pLeft = NULL;
pExpr->pRight = NULL;
}
}
}
SSqlNode *tSetQuerySqlNode(SToken *pSelectToken, SArray *pSelNodeList, SRelationInfo *pFrom, tSqlExpr *pWhere,
SArray *pGroupby, SArray *pSortOrder, SIntervalVal *pInterval,
SSessionWindowVal *pSession, SWindowStateVal *pWindowStateVal, SToken *pSliding, SArray *pFill, SLimit *pLimit,
......
......@@ -13,13 +13,82 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ttime.h"
#include "parserInt.h"
#include "parserUtil.h"
#include "tmsgtype.h"
static int32_t setInvalidOperatorErrMsg(char* dst, int32_t dstBufLen, const char* msg) {
strncpy(dst, msg, dstBufLen);
return TSDB_CODE_TSC_INVALID_OPERATION;
static int32_t evaluateImpl(tSqlExpr* pExpr, int32_t tsPrecision) {
int32_t code = 0;
if (pExpr->type == SQL_NODE_EXPR) {
code = evaluateImpl(pExpr->pLeft, tsPrecision);
if (code != TSDB_CODE_SUCCESS) {
return code;
}
code = evaluateImpl(pExpr->pRight, tsPrecision);
if (code != TSDB_CODE_SUCCESS) {
return code;
}
if (pExpr->pLeft->type == SQL_NODE_VALUE && pExpr->pRight->type == SQL_NODE_VALUE) {
tSqlExpr* pLeft = pExpr->pLeft;
tSqlExpr* pRight = pExpr->pRight;
if ((pLeft->tokenId == TK_TIMESTAMP && (pRight->tokenId == TK_INTEGER || pRight->tokenId == TK_FLOAT)) ||
((pRight->tokenId == TK_TIMESTAMP && (pLeft->tokenId == TK_INTEGER || pLeft->tokenId == TK_FLOAT)))) {
return TSDB_CODE_TSC_SQL_SYNTAX_ERROR;
} else if (pLeft->tokenId == TK_TIMESTAMP && pRight->tokenId == TK_TIMESTAMP) {
tSqlExprEvaluate(pExpr);
} else {
tSqlExprEvaluate(pExpr);
}
} else {
// Other types of expressions are not evaluated, they will be handled during the validation of the abstract syntax tree.
}
} else if (pExpr->type == SQL_NODE_VALUE) {
if (pExpr->tokenId == TK_NOW) {
pExpr->value.i64 = taosGetTimestamp(tsPrecision);
pExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pExpr->tokenId = TK_TIMESTAMP;
} else if (pExpr->tokenId == TK_VARIABLE) {
char unit = 0;
SToken* pToken = &pExpr->exprToken;
int32_t ret = parseAbsoluteDuration(pToken->z, pToken->n, &pExpr->value.i64, &unit, tsPrecision);
if (ret != TSDB_CODE_SUCCESS) {
return TSDB_CODE_TSC_SQL_SYNTAX_ERROR;
}
pExpr->value.nType = TSDB_DATA_TYPE_BIGINT;
pExpr->tokenId = TK_TIMESTAMP;
} else if (pExpr->tokenId == TK_NULL) {
pExpr->value.nType = TSDB_DATA_TYPE_NULL;
} else if (pExpr->tokenId == TK_INTEGER || pExpr->tokenId == TK_STRING || pExpr->tokenId == TK_FLOAT || pExpr->tokenId == TK_BOOL) {
SToken* pToken = &pExpr->exprToken;
int32_t tokenType = pToken->type;
toTSDBType(tokenType);
taosVariantCreate(&pExpr->value, pToken->z, pToken->n, tokenType);
}
return TSDB_CODE_SUCCESS;
// other types of data are handled in the parent level.
}
return TSDB_CODE_SUCCESS;
}
int32_t evaluateSqlNode(SSqlNode* pNode, int32_t tsPrecision, char* msg, int32_t msgBufLen) {
assert(pNode != NULL && msg != NULL && msgBufLen > 0);
if (pNode->pWhere == NULL) {
return TSDB_CODE_SUCCESS;
}
int32_t code = evaluateImpl(pNode->pWhere, tsPrecision);
if (code != TSDB_CODE_SUCCESS) {
strncpy(msg, "invalid time expression in sql", msgBufLen);
return code;
}
return code;
}
int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQueryStmtInfo* pQueryInfo, int64_t id, char* msgBuf, int32_t msgBufLen) {
......@@ -37,15 +106,15 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
const char* msg2 = "invalid name";
SToken* pzName = taosArrayGet(pInfo->pMiscInfo->a, 0);
if ((pInfo->type != TSDB_SQL_DROP_DNODE) && (parserValidateNameToken(pzName) != TSDB_CODE_SUCCESS)) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
if ((pInfo->type != TSDB_SQL_DROP_DNODE) && (parserValidateIdToken(pzName) != TSDB_CODE_SUCCESS)) {
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
if (pInfo->type == TSDB_SQL_DROP_DB) {
assert(taosArrayGetSize(pInfo->pMiscInfo->a) == 1);
code = tNameSetDbName(&pTableMetaInfo->name, getAccountId(pSql), pzName);
if (code != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
} else if (pInfo->type == TSDB_SQL_DROP_TABLE) {
......@@ -62,7 +131,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
strncpy(pCmd->payload, pzName->z, pzName->n);
} else { // drop user/account
if (pzName->n >= TSDB_USER_LEN) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg3);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg3);
}
strncpy(pCmd->payload, pzName->z, pzName->n);
......@@ -76,12 +145,12 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SToken* pToken = taosArrayGet(pInfo->pMiscInfo->a, 0);
if (tscValidateName(pToken) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg);
}
int32_t ret = tNameSetDbName(&pTableMetaInfo->name, getAccountId(pSql), pToken);
if (ret != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg);
}
break;
......@@ -116,19 +185,19 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SCreateDbInfo* pCreateDB = &(pInfo->pMiscInfo->dbOpt);
if (pCreateDB->dbname.n >= TSDB_DB_NAME_LEN) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
char buf[TSDB_DB_NAME_LEN] = {0};
SToken token = taosTokenDup(&pCreateDB->dbname, buf, tListLen(buf));
if (tscValidateName(&token) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
int32_t ret = tNameSetDbName(&pTableMetaInfo->name, getAccountId(pSql), &token);
if (ret != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
if (parseCreateDBOptions(pCmd, pCreateDB) != TSDB_CODE_SUCCESS) {
......@@ -142,7 +211,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
const char* msg = "invalid host name (ip address)";
if (taosArrayGetSize(pInfo->pMiscInfo->a) > 1) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg);
}
SToken* id = taosArrayGet(pInfo->pMiscInfo->a, 0);
......@@ -166,11 +235,11 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
}
if (pName->n >= TSDB_USER_LEN) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg3);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg3);
}
if (tscValidateName(pName) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
SCreateAcctInfo* pAcctOpt = &pInfo->pMiscInfo->acctOpt;
......@@ -180,7 +249,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
} else if (strncmp(pAcctOpt->stat.z, "all", 3) == 0 && pAcctOpt->stat.n == 3) {
} else if (strncmp(pAcctOpt->stat.z, "no", 2) == 0 && pAcctOpt->stat.n == 2) {
} else {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
}
......@@ -192,7 +261,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SToken* pToken = taosArrayGet(pInfo->pMiscInfo->a, 0);
if (tscValidateName(pToken) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
// additional msg has been attached already
code = tscSetTableFullName(&pTableMetaInfo->name, pToken, pSql);
......@@ -208,7 +277,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SToken* pToken = taosArrayGet(pInfo->pMiscInfo->a, 0);
if (tscValidateName(pToken) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
code = tscSetTableFullName(&pTableMetaInfo->name, pToken, pSql);
......@@ -223,11 +292,11 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SToken* pToken = taosArrayGet(pInfo->pMiscInfo->a, 0);
if (tscValidateName(pToken) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
if (pToken->n > TSDB_DB_NAME_LEN) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
return tNameSetDbName(&pTableMetaInfo->name, getAccountId(pSql), pToken);
}
......@@ -240,7 +309,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
/* validate the parameter names and options */
if (validateDNodeConfig(pMiscInfo) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
char* pMsg = pCmd->payload;
......@@ -254,7 +323,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
strncpy(pCfg->ep, t0->z, t0->n);
if (validateEp(pCfg->ep) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg3);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg3);
}
strncpy(pCfg->config, t1->z, t1->n);
......@@ -283,11 +352,11 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
SToken* pPwd = &pUser->passwd;
if (pName->n >= TSDB_USER_LEN) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg3);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg3);
}
if (tscValidateName(pName) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg2);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg2);
}
if (pCmd->command == TSDB_SQL_CREATE_USER) {
......@@ -311,10 +380,10 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
} else if (strncasecmp(pPrivilege->z, "write", 5) == 0 && pPrivilege->n == 5) {
pCmd->count = 3;
} else {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg5);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg5);
}
} else {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg7);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg7);
}
}
......@@ -327,7 +396,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
// validate the parameter names and options
if (validateLocalConfig(pMiscInfo) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg);
}
int32_t numOfToken = (int32_t) taosArrayGetSize(pMiscInfo->a);
......@@ -382,7 +451,7 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
tscTrace("0x%"PRIx64" start to parse the %dth subclause, total:%"PRIzu, pSql->self, i, size);
if (size > 1 && pSqlNode->from && pSqlNode->from->type == SQL_NODE_FROM_SUBQUERY) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
// normalizeSqlNode(pSqlNode); // normalize the column name in each function
......@@ -448,19 +517,19 @@ int32_t qParserValidateSqlNode(struct SCatalog* pCatalog, SSqlInfo* pInfo, SQuer
assert(taosArrayGetSize(pInfo->pMiscInfo->a) == 1);
code = tNameSetDbName(&pTableMetaInfo->name, getAccountId(pSql), pzName);
if (code != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg1);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg1);
}
break;
}
case TSDB_SQL_COMPACT_VNODE:{
const char* msg = "invalid compact";
if (setCompactVnodeInfo(pSql, pInfo) != TSDB_CODE_SUCCESS) {
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, msg);
return setInvalidOperatorMsg(msgBuf, msgBufLen, msg);
}
break;
}
default:
return setInvalidOperatorErrMsg(msgBuf, msgBufLen, "not support sql expression");
return setInvalidOperatorMsg(msgBuf, msgBufLen, "not support sql expression");
}
#endif
......
......@@ -16,6 +16,7 @@
#include "parserInt.h"
#include "ttoken.h"
#include "astGenerator.h"
#include "parserUtil.h"
bool qIsInsertSql(const char* pStr, size_t length) {
return false;
......@@ -50,6 +51,216 @@ int32_t qParserConvertSql(const char* pStr, size_t length, char** pConvertSql) {
return 0;
}
int32_t qParserExtractRequestedMetaInfo(const SArray* pSqlNodeList, SMetaReq* pMetaInfo) {
return 0;
static int32_t getTableNameFromSubquery(SSqlNode* pSqlNode, SArray* tableNameList, char* msgBuf) {
int32_t numOfSub = (int32_t)taosArrayGetSize(pSqlNode->from->list);
for (int32_t j = 0; j < numOfSub; ++j) {
SRelElementPair* sub = taosArrayGet(pSqlNode->from->list, j);
int32_t num = (int32_t)taosArrayGetSize(sub->pSubquery);
for (int32_t i = 0; i < num; ++i) {
SSqlNode* p = taosArrayGetP(sub->pSubquery, i);
if (p->from->type == SQL_NODE_FROM_TABLELIST) {
int32_t code = getTableNameFromSqlNode(p, tableNameList, msgBuf);
if (code != TSDB_CODE_SUCCESS) {
return code;
}
} else {
getTableNameFromSubquery(p, tableNameList, msgBuf);
}
}
}
return TSDB_CODE_SUCCESS;
}
static int32_t getTableNameFromSqlNode(SSqlNode* pSqlNode, SArray* tableNameList, char* msg, int32_t msgBufLen) {
const char* msg1 = "invalid table name";
int32_t numOfTables = (int32_t) taosArrayGetSize(pSqlNode->from->list);
assert(pSqlNode->from->type == SQL_NODE_FROM_TABLELIST);
for(int32_t j = 0; j < numOfTables; ++j) {
SRelElementPair* item = taosArrayGet(pSqlNode->from->list, j);
SToken* t = &item->tableName;
if (t->type == TK_INTEGER || t->type == TK_FLOAT) {
return parserSetInvalidOperatorMsg(msg, msgBufLen, msg1);
}
tscDequoteAndTrimToken(t);
if (parserValidateIdToken(t) != TSDB_CODE_SUCCESS) {
return parserSetInvalidOperatorMsg(msg, msgBufLen, msg1);
}
SName name = {0};
int32_t code = tscSetTableFullName(&name, t, pSql);
if (code != TSDB_CODE_SUCCESS) {
return code;
}
taosArrayPush(tableNameList, &name);
}
return TSDB_CODE_SUCCESS;
}
int32_t qParserExtractRequestedMetaInfo(const SArray* pSqlNodeList, SMetaReq* pMetaInfo, char* msg, int32_t msgBufLen) {
int32_t code = TSDB_CODE_SUCCESS;
SArray* tableNameList = NULL;
SArray* pVgroupList = NULL;
SArray* plist = NULL;
STableMeta* pTableMeta = NULL;
// size_t tableMetaCapacity = 0;
// SQueryInfo* pQueryInfo = tscGetQueryInfo(pCmd);
// pCmd->pTableMetaMap = taosHashInit(4, taosGetDefaultHashFunction(TSDB_DATA_TYPE_BINARY), false, HASH_NO_LOCK);
tableNameList = taosArrayInit(4, sizeof(SName));
size_t size = taosArrayGetSize(pSqlNodeList);
for (int32_t i = 0; i < size; ++i) {
SSqlNode* pSqlNode = taosArrayGetP(pSqlNodeList, i);
if (pSqlNode->from == NULL) {
goto _end;
}
// load the table meta in the from clause
if (pSqlNode->from->type == SQL_NODE_FROM_TABLELIST) {
code = getTableNameFromSqlNode(pSqlNode, tableNameList, msg, msgBufLen);
if (code != TSDB_CODE_SUCCESS) {
goto _end;
}
} else {
code = getTableNameFromSubquery(pSqlNode, tableNameList, msg, msgBufLen);
if (code != TSDB_CODE_SUCCESS) {
goto _end;
}
}
}
char name[TSDB_TABLE_FNAME_LEN] = {0};
plist = taosArrayInit(4, POINTER_BYTES);
pVgroupList = taosArrayInit(4, POINTER_BYTES);
taosArraySort(tableNameList, tnameComparFn);
taosArrayRemoveDuplicate(tableNameList, tnameComparFn, NULL);
STableMeta* pSTMeta = (STableMeta *)(pSql->pBuf);
size_t numOfTables = taosArrayGetSize(tableNameList);
for (int32_t i = 0; i < numOfTables; ++i) {
SName* pname = taosArrayGet(tableNameList, i);
tNameExtractFullName(pname, name);
size_t len = strlen(name);
if (NULL == taosHashGetCloneExt(tscTableMetaMap, name, len, NULL, (void **)&pTableMeta, &tableMetaCapacity)) {
// not found
tfree(pTableMeta);
}
if (pTableMeta && pTableMeta->id.uid > 0) {
tscDebug("0x%"PRIx64" retrieve table meta %s from local buf", pSql->self, name);
// avoid mem leak, may should update pTableMeta
void* pVgroupIdList = NULL;
if (pTableMeta->tableType == TSDB_CHILD_TABLE) {
code = tscCreateTableMetaFromSTableMeta((STableMeta **)(&pTableMeta), name, &tableMetaCapacity, (STableMeta **)(&pSTMeta));
pSql->pBuf = (void *)pSTMeta;
// create the child table meta from super table failed, try load it from mnode
if (code != TSDB_CODE_SUCCESS) {
char* t = strdup(name);
taosArrayPush(plist, &t);
continue;
}
} else if (pTableMeta->tableType == TSDB_SUPER_TABLE) {
// the vgroup list of super table is not kept in local buffer, so here need retrieve it from the mnode each time
tscDebug("0x%"PRIx64" try to acquire cached super table %s vgroup id list", pSql->self, name);
void* pv = taosCacheAcquireByKey(tscVgroupListBuf, name, len);
if (pv == NULL) {
char* t = strdup(name);
taosArrayPush(pVgroupList, &t);
tscDebug("0x%"PRIx64" failed to retrieve stable %s vgroup id list in cache, try fetch from mnode", pSql->self, name);
} else {
tFilePage* pdata = (tFilePage*) pv;
pVgroupIdList = taosArrayInit((size_t) pdata->num, sizeof(int32_t));
if (pVgroupIdList == NULL) {
return TSDB_CODE_TSC_OUT_OF_MEMORY;
}
taosArrayAddBatch(pVgroupIdList, pdata->data, (int32_t) pdata->num);
taosCacheRelease(tscVgroupListBuf, &pv, false);
}
}
if (taosHashGet(pCmd->pTableMetaMap, name, len) == NULL) {
STableMeta* pMeta = tscTableMetaDup(pTableMeta);
STableMetaVgroupInfo tvi = { .pTableMeta = pMeta, .vgroupIdList = pVgroupIdList};
taosHashPut(pCmd->pTableMetaMap, name, len, &tvi, sizeof(STableMetaVgroupInfo));
}
} else {
// Add to the retrieve table meta array list.
// If the tableMeta is missing, the cached vgroup list for the corresponding super table will be ignored.
tscDebug("0x%"PRIx64" failed to retrieve table meta %s from local buf", pSql->self, name);
char* t = strdup(name);
taosArrayPush(plist, &t);
}
}
size_t funcSize = 0;
if (pInfo->funcs) {
funcSize = taosArrayGetSize(pInfo->funcs);
}
if (funcSize > 0) {
for (size_t i = 0; i < funcSize; ++i) {
SToken* t = taosArrayGet(pInfo->funcs, i);
if (NULL == t) {
continue;
}
if (t->n >= TSDB_FUNC_NAME_LEN) {
code = tscSQLSyntaxErrMsg(tscGetErrorMsgPayload(pCmd), "too long function name", t->z);
if (code != TSDB_CODE_SUCCESS) {
goto _end;
}
}
int32_t functionId = isValidFunction(t->z, t->n);
if (functionId < 0) {
struct SUdfInfo info = {0};
info.name = strndup(t->z, t->n);
if (pQueryInfo->pUdfInfo == NULL) {
pQueryInfo->pUdfInfo = taosArrayInit(4, sizeof(struct SUdfInfo));
}
info.functionId = (int32_t)taosArrayGetSize(pQueryInfo->pUdfInfo) * (-1) - 1;;
taosArrayPush(pQueryInfo->pUdfInfo, &info);
}
}
}
// load the table meta for a given table name list
if (taosArrayGetSize(plist) > 0 || taosArrayGetSize(pVgroupList) > 0 || (pQueryInfo->pUdfInfo && taosArrayGetSize(pQueryInfo->pUdfInfo) > 0)) {
code = getMultiTableMetaFromMnode(pSql, plist, pVgroupList, pQueryInfo->pUdfInfo, tscTableMetaCallBack, true);
}
_end:
if (plist != NULL) {
taosArrayDestroyEx(plist, freeElem);
}
if (pVgroupList != NULL) {
taosArrayDestroyEx(pVgroupList, freeElem);
}
if (tableNameList != NULL) {
taosArrayDestroy(tableNameList);
}
tfree(pTableMeta);
return code;
}
\ No newline at end of file
......@@ -2,7 +2,7 @@
#include "taoserror.h"
#include "tutil.h"
int32_t parserValidateNameToken(SToken* pToken) {
int32_t parserValidateIdToken(SToken* pToken) {
if (pToken == NULL || pToken->z == NULL || pToken->type != TK_ID) {
return TSDB_CODE_TSC_INVALID_OPERATION;
}
......@@ -57,4 +57,9 @@ int32_t parserValidateNameToken(SToken* pToken) {
}
return TSDB_CODE_SUCCESS;
}
int32_t parserSetInvalidOperatorMsg(char* dst, int32_t dstBufLen, const char* msg) {
strncpy(dst, msg, dstBufLen);
return TSDB_CODE_TSC_INVALID_OPERATION;
}
\ No newline at end of file
......@@ -411,6 +411,7 @@ uint32_t tGetToken(char* z, uint32_t* tokenId) {
*tokenId = TK_QUESTION;
return 1;
}
case '`':
case '\'':
case '"': {
int delim = z[0];
......@@ -434,7 +435,7 @@ uint32_t tGetToken(char* z, uint32_t* tokenId) {
if (z[i]) i++;
if (strEnd) {
*tokenId = TK_STRING;
*tokenId = (delim == '`')? TK_ID:TK_STRING;
return i;
}
......
......@@ -14,6 +14,7 @@
#include "ttoken.h"
#include "astGenerator.h"
#include "parserUtil.h"
#include "parserInt.h"
namespace {
int32_t testValidateName(char* name) {
......@@ -23,7 +24,7 @@ int32_t testValidateName(char* name) {
token.type = 0;
tGetToken(name, &token.type);
return parserValidateNameToken(&token);
return parserValidateIdToken(&token);
}
SToken createToken(char* s) {
......@@ -667,4 +668,29 @@ TEST(testCase, isValidNumber_test) {
TEST(testCase, generateAST_test) {
SSqlInfo info = doGenerateAST("select * from t1 where ts < now");
ASSERT_EQ(info.valid, true);
}
\ No newline at end of file
SSqlInfo info1 = doGenerateAST("select * from `t.1abc` where ts<now+2h and col < 20+99");
ASSERT_EQ(info1.valid, true);
char msg[128] = {0};
SSqlNode* pNode = (SSqlNode*) taosArrayGetP(((SArray*)info1.list), 0);
int32_t code = evaluateSqlNode(pNode, TSDB_TIME_PRECISION_NANO, msg, sizeof(msg));
ASSERT_EQ(code, 0);
SSqlInfo info2 = doGenerateAST("select * from abc where ts<now+2");
SSqlNode* pNode2 = (SSqlNode*) taosArrayGetP(((SArray*)info2.list), 0);
code = evaluateSqlNode(pNode2, TSDB_TIME_PRECISION_MILLI, msg, sizeof(msg));
ASSERT_NE(code, 0);
}
TEST(testCase, evaluateAST_test) {
SSqlInfo info1 = doGenerateAST("select a, b+22 from `t.1abc` where ts<now+2h and col < 20 + 99");
ASSERT_EQ(info1.valid, true);
char msg[128] = {0};
SSqlNode* pNode = (SSqlNode*) taosArrayGetP(((SArray*)info1.list), 0);
int32_t code = evaluateSqlNode(pNode, TSDB_TIME_PRECISION_NANO, msg, sizeof(msg));
ASSERT_EQ(code, 0);
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册