parInsertSml.c 14.2 KB
Newer Older
X
Xiaoyu Wang 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 * Copyright (c) 2019 TAOS Data, Inc. <jhtao@taosdata.com>
 *
 * This program is free software: you can use, redistribute, and/or modify
 * it under the terms of the GNU Affero General Public License, version 3
 * or later ("AGPL"), as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "parInsertUtil.h"
#include "parInt.h"
#include "parToken.h"
#include "ttime.h"

wmmhello's avatar
wmmhello 已提交
21 22 23 24 25 26 27
static void clearColValArray(SArray* pCols) {
  int32_t num = taosArrayGetSize(pCols);
  for (int32_t i = 0; i < num; ++i) {
    SColVal* pCol = taosArrayGet(pCols, i);
    if (TSDB_DATA_TYPE_NCHAR == pCol->type) {
      taosMemoryFreeClear(pCol->value.pData);
    }
28 29
    pCol->flag = CV_FLAG_NONE;
    pCol->value.val = 0;
wmmhello's avatar
wmmhello 已提交
30 31 32
  }
}

X
Xiaoyu Wang 已提交
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
int32_t qCreateSName(SName* pName, const char* pTableName, int32_t acctId, char* dbName, char* msgBuf,
                     int32_t msgBufLen) {
  SMsgBuf msg = {.buf = msgBuf, .len = msgBufLen};
  SToken  sToken;
  int32_t code = 0;
  char*   tbName = NULL;

  NEXT_TOKEN(pTableName, sToken);

  if (sToken.n == 0) {
    return buildInvalidOperationMsg(&msg, "empty table name");
  }

  code = insCreateSName(pName, &sToken, acctId, dbName, &msg);
  if (code) {
    return code;
  }

  NEXT_TOKEN(pTableName, sToken);

  if (sToken.n > 0) {
    return buildInvalidOperationMsg(&msg, "table name format is wrong");
  }

  return TSDB_CODE_SUCCESS;
}

60 61 62 63
static int32_t smlBoundColumnData(SArray* cols, SBoundColInfo* pBoundInfo, SSchema* pSchema, bool isTag) {
  bool* pUseCols = taosMemoryCalloc(pBoundInfo->numOfCols, sizeof(bool));
  if (NULL == pUseCols) {
    return TSDB_CODE_OUT_OF_MEMORY;
X
Xiaoyu Wang 已提交
64 65
  }

66 67 68 69
  pBoundInfo->numOfBound = 0;
  int16_t lastColIdx = -1;  // last column found
  int32_t code = TSDB_CODE_SUCCESS;

X
Xiaoyu Wang 已提交
70
  for (int i = 0; i < taosArrayGetSize(cols); ++i) {
71
    SSmlKv*  kv = taosArrayGet(cols, i);
X
Xiaoyu Wang 已提交
72 73
    SToken   sToken = {.n = kv->keyLen, .z = (char*)kv->key};
    col_id_t t = lastColIdx + 1;
74
    col_id_t index = ((t == 0 && !isTag) ? 0 : insFindCol(&sToken, t, pBoundInfo->numOfCols, pSchema));
D
dapan1121 已提交
75
    uTrace("SML, index:%d, t:%d, ncols:%d", index, t, pBoundInfo->numOfCols);
X
Xiaoyu Wang 已提交
76 77 78
    if (index < 0 && t > 0) {
      index = insFindCol(&sToken, 0, t, pSchema);
    }
79

X
Xiaoyu Wang 已提交
80 81
    if (index < 0) {
      uError("smlBoundColumnData. index:%d", index);
82 83
      code = TSDB_CODE_SML_INVALID_DATA;
      goto end;
X
Xiaoyu Wang 已提交
84
    }
85
    if (pUseCols[index]) {
X
Xiaoyu Wang 已提交
86
      uError("smlBoundColumnData. already set. index:%d", index);
87 88
      code = TSDB_CODE_SML_INVALID_DATA;
      goto end;
X
Xiaoyu Wang 已提交
89 90
    }
    lastColIdx = index;
91 92 93
    pUseCols[index] = true;
    pBoundInfo->pColIndex[pBoundInfo->numOfBound] = index;
    ++pBoundInfo->numOfBound;
X
Xiaoyu Wang 已提交
94 95
  }

96 97
end:
  taosMemoryFree(pUseCols);
X
Xiaoyu Wang 已提交
98

99
  return code;
X
Xiaoyu Wang 已提交
100 101 102 103 104 105 106 107 108 109 110 111
}

/**
 * @brief No json tag for schemaless
 *
 * @param cols
 * @param tags
 * @param pSchema
 * @param ppTag
 * @param msg
 * @return int32_t
 */
112
static int32_t smlBuildTagRow(SArray* cols, SBoundColInfo* tags, SSchema* pSchema, STag** ppTag, SArray** tagName,
X
Xiaoyu Wang 已提交
113 114 115
                              SMsgBuf* msg) {
  SArray* pTagArray = taosArrayInit(tags->numOfBound, sizeof(STagVal));
  if (!pTagArray) {
S
Shengliang Guan 已提交
116
    return TSDB_CODE_OUT_OF_MEMORY;
X
Xiaoyu Wang 已提交
117 118 119
  }
  *tagName = taosArrayInit(8, TSDB_COL_NAME_LEN);
  if (!*tagName) {
S
Shengliang Guan 已提交
120
    return TSDB_CODE_OUT_OF_MEMORY;
X
Xiaoyu Wang 已提交
121 122 123 124
  }

  int32_t code = TSDB_CODE_SUCCESS;
  for (int i = 0; i < tags->numOfBound; ++i) {
125
    SSchema* pTagSchema = &pSchema[tags->pColIndex[i]];
126
    SSmlKv*  kv = taosArrayGet(cols, i);
X
Xiaoyu Wang 已提交
127

128 129 130 131 132 133
    if(kv->keyLen != strlen(pTagSchema->name) || memcmp(kv->key, pTagSchema->name, kv->keyLen) != 0 || kv->type != pTagSchema->type){
      code = TSDB_CODE_SML_INVALID_DATA;
      uError("SML smlBuildCol error col not same %s", pTagSchema->name);
      goto end;
    }

X
Xiaoyu Wang 已提交
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    taosArrayPush(*tagName, pTagSchema->name);
    STagVal val = {.cid = pTagSchema->colId, .type = pTagSchema->type};
    //    strcpy(val.colName, pTagSchema->name);
    if (pTagSchema->type == TSDB_DATA_TYPE_BINARY) {
      val.pData = (uint8_t*)kv->value;
      val.nData = kv->length;
    } else if (pTagSchema->type == TSDB_DATA_TYPE_NCHAR) {
      int32_t output = 0;
      void*   p = taosMemoryCalloc(1, kv->length * TSDB_NCHAR_SIZE);
      if (p == NULL) {
        code = TSDB_CODE_OUT_OF_MEMORY;
        goto end;
      }
      if (!taosMbsToUcs4(kv->value, kv->length, (TdUcs4*)(p), kv->length * TSDB_NCHAR_SIZE, &output)) {
        if (errno == E2BIG) {
          taosMemoryFree(p);
          code = generateSyntaxErrMsg(msg, TSDB_CODE_PAR_VALUE_TOO_LONG, pTagSchema->name);
          goto end;
        }
        char buf[512] = {0};
        snprintf(buf, tListLen(buf), " taosMbsToUcs4 error:%s", strerror(errno));
        taosMemoryFree(p);
        code = buildSyntaxErrMsg(msg, buf, kv->value);
        goto end;
      }
      val.pData = p;
      val.nData = output;
    } else {
      memcpy(&val.i64, &(kv->value), kv->length);
    }
    taosArrayPush(pTagArray, &val);
  }

  code = tTagNew(pTagArray, 1, false, ppTag);
end:
  for (int i = 0; i < taosArrayGetSize(pTagArray); ++i) {
    STagVal* p = (STagVal*)taosArrayGet(pTagArray, i);
    if (p->type == TSDB_DATA_TYPE_NCHAR) {
      taosMemoryFree(p->pData);
    }
  }
  taosArrayDestroy(pTagArray);
  return code;
}

X
Xiaoyu Wang 已提交
179
STableDataCxt* smlInitTableDataCtx(SQuery* query, STableMeta* pTableMeta) {
180
  STableDataCxt* pTableCxt = NULL;
X
Xiaoyu Wang 已提交
181 182 183
  SVCreateTbReq* pCreateTbReq = NULL;
  int            ret = insGetTableDataCxt(((SVnodeModifyOpStmt*)(query->pRoot))->pTableBlockHashObj, &pTableMeta->uid,
                                          sizeof(pTableMeta->uid), pTableMeta, &pCreateTbReq, &pTableCxt, false);
184 185 186 187 188 189 190 191 192 193 194
  if (ret != TSDB_CODE_SUCCESS) {
    return NULL;
  }

  ret = initTableColSubmitData(pTableCxt);
  if (ret != TSDB_CODE_SUCCESS) {
    return NULL;
  }
  return pTableCxt;
}

X
Xiaoyu Wang 已提交
195
int32_t smlBuildRow(STableDataCxt* pTableCxt) {
196
  SRow** pRow = taosArrayReserve(pTableCxt->pData->aRowP, 1);
X
Xiaoyu Wang 已提交
197
  int    ret = tRowBuild(pTableCxt->pValues, pTableCxt->pSchema, pRow);
198 199 200 201 202 203 204
  if (TSDB_CODE_SUCCESS != ret) {
    return ret;
  }
  insCheckTableDataOrder(pTableCxt, TD_ROW_KEY(*pRow));
  return TSDB_CODE_SUCCESS;
}

X
Xiaoyu Wang 已提交
205 206
int32_t smlBuildCol(STableDataCxt* pTableCxt, SSchema* schema, void* data, int32_t index) {
  int      ret = TSDB_CODE_SUCCESS;
207 208
  SSchema* pColSchema = schema + index;
  SColVal* pVal = taosArrayGet(pTableCxt->pValues, index);
X
Xiaoyu Wang 已提交
209
  SSmlKv*  kv = (SSmlKv*)data;
wmmhello's avatar
wmmhello 已提交
210
  if(kv->keyLen != strlen(pColSchema->name) || memcmp(kv->key, pColSchema->name, kv->keyLen) != 0 || kv->type != pColSchema->type){
wmmhello's avatar
wmmhello 已提交
211
    ret = TSDB_CODE_SML_INVALID_DATA;
wmmhello's avatar
wmmhello 已提交
212
    uError("SML smlBuildCol error col not same %s", pColSchema->name);
wmmhello's avatar
wmmhello 已提交
213 214
    goto end;
  }
X
Xiaoyu Wang 已提交
215
  if (kv->type == TSDB_DATA_TYPE_NCHAR) {
216
    int32_t len = 0;
wmmhello's avatar
wmmhello 已提交
217 218 219 220 221 222
    int64_t size = pColSchema->bytes - VARSTR_HEADER_SIZE;
    if(size <= 0){
      ret = TSDB_CODE_SML_INVALID_DATA;
      goto end;
    }
    char*   pUcs4 = taosMemoryCalloc(1, size);
223 224 225 226
    if (NULL == pUcs4) {
      ret = TSDB_CODE_OUT_OF_MEMORY;
      goto end;
    }
wmmhello's avatar
wmmhello 已提交
227
    if (!taosMbsToUcs4(kv->value, kv->length, (TdUcs4*)pUcs4, size, &len)) {
228
      if (errno == E2BIG) {
wmmhello's avatar
wmmhello 已提交
229
        taosMemoryFree(pUcs4);
230 231 232
        ret = TSDB_CODE_PAR_VALUE_TOO_LONG;
        goto end;
      }
wmmhello's avatar
wmmhello 已提交
233
      taosMemoryFree(pUcs4);
234 235 236 237 238
      ret = TSDB_CODE_TSC_INVALID_VALUE;
      goto end;
    }
    pVal->value.pData = pUcs4;
    pVal->value.nData = len;
X
Xiaoyu Wang 已提交
239
  } else if (kv->type == TSDB_DATA_TYPE_BINARY) {
240
    pVal->value.nData = kv->length;
X
Xiaoyu Wang 已提交
241
    pVal->value.pData = (uint8_t*)kv->value;
242 243 244 245 246 247 248 249 250
  } else {
    memcpy(&pVal->value.val, &(kv->value), kv->length);
  }
  pVal->flag = CV_FLAG_VALUE;

end:
  return ret;
}

X
Xiaoyu Wang 已提交
251 252
int32_t smlBindData(SQuery* query, bool dataFormat, SArray* tags, SArray* colsSchema, SArray* cols,
                    STableMeta* pTableMeta, char* tableName, const char* sTableName, int32_t sTableNameLen, int32_t ttl,
253
                    char* msgBuf, int32_t msgBufLen) {
X
Xiaoyu Wang 已提交
254 255
  SMsgBuf pBuf = {.buf = msgBuf, .len = msgBufLen};

256 257 258 259
  SSchema*       pTagsSchema = getTableTagSchema(pTableMeta);
  SBoundColInfo  bindTags = {0};
  SVCreateTbReq* pCreateTblReq = NULL;
  SArray*        tagName = NULL;
260 261 262

  insInitBoundColsInfo(getNumOfTags(pTableMeta), &bindTags);
  int ret = smlBoundColumnData(tags, &bindTags, pTagsSchema, true);
X
Xiaoyu Wang 已提交
263 264
  if (ret != TSDB_CODE_SUCCESS) {
    buildInvalidOperationMsg(&pBuf, "bound tags error");
265
    goto end;
X
Xiaoyu Wang 已提交
266
  }
267

268
  STag* pTag = NULL;
269 270

  ret = smlBuildTagRow(tags, &bindTags, pTagsSchema, &pTag, &tagName, &pBuf);
X
Xiaoyu Wang 已提交
271
  if (ret != TSDB_CODE_SUCCESS) {
272
    goto end;
X
Xiaoyu Wang 已提交
273 274
  }

275 276 277 278 279
  pCreateTblReq = taosMemoryCalloc(1, sizeof(SVCreateTbReq));
  if (NULL == pCreateTblReq) {
    ret = TSDB_CODE_OUT_OF_MEMORY;
    goto end;
  }
280 281
  insBuildCreateTbReq(pCreateTblReq, tableName, pTag, pTableMeta->suid, NULL, tagName, pTableMeta->tableInfo.numOfTags,
                      ttl);
X
Xiaoyu Wang 已提交
282

283 284
  pCreateTblReq->ctb.stbName = taosMemoryCalloc(1, sTableNameLen + 1);
  memcpy(pCreateTblReq->ctb.stbName, sTableName, sTableNameLen);
X
Xiaoyu Wang 已提交
285

X
Xiaoyu Wang 已提交
286 287 288
  if (dataFormat) {
    STableDataCxt** pTableCxt = (STableDataCxt**)taosHashGet(((SVnodeModifyOpStmt*)(query->pRoot))->pTableBlockHashObj,
                                                             &pTableMeta->uid, sizeof(pTableMeta->uid));
289 290 291 292 293 294
    if (NULL == pTableCxt) {
      ret = buildInvalidOperationMsg(&pBuf, "dataformat true. get tableDataCtx error");
      goto end;
    }
    (*pTableCxt)->pData->flags |= SUBMIT_REQ_AUTO_CREATE_TABLE;
    (*pTableCxt)->pData->pCreateTbReq = pCreateTblReq;
295 296
    (*pTableCxt)->pMeta->uid = pTableMeta->uid;
    (*pTableCxt)->pMeta->vgId = pTableMeta->vgId;
297 298 299 300
    pCreateTblReq = NULL;
    goto end;
  }

301
  STableDataCxt* pTableCxt = NULL;
X
Xiaoyu Wang 已提交
302
  ret = insGetTableDataCxt(((SVnodeModifyOpStmt*)(query->pRoot))->pTableBlockHashObj, &pTableMeta->uid,
303
                           sizeof(pTableMeta->uid), pTableMeta, &pCreateTblReq, &pTableCxt, false);
X
Xiaoyu Wang 已提交
304
  if (ret != TSDB_CODE_SUCCESS) {
305 306
    buildInvalidOperationMsg(&pBuf, "insGetTableDataCxt error");
    goto end;
X
Xiaoyu Wang 已提交
307 308 309
  }

  SSchema* pSchema = getTableColumnSchema(pTableMeta);
310
  ret = smlBoundColumnData(colsSchema, &pTableCxt->boundColsInfo, pSchema, false);
X
Xiaoyu Wang 已提交
311 312
  if (ret != TSDB_CODE_SUCCESS) {
    buildInvalidOperationMsg(&pBuf, "bound cols error");
313
    goto end;
X
Xiaoyu Wang 已提交
314 315
  }

316 317 318 319 320
  ret = initTableColSubmitData(pTableCxt);
  if (ret != TSDB_CODE_SUCCESS) {
    buildInvalidOperationMsg(&pBuf, "initTableColSubmitData error");
    goto end;
  }
X
Xiaoyu Wang 已提交
321 322 323

  int32_t rowNum = taosArrayGetSize(cols);
  if (rowNum <= 0) {
324 325
    ret = buildInvalidOperationMsg(&pBuf, "cols size <= 0");
    goto end;
X
Xiaoyu Wang 已提交
326
  }
327

X
Xiaoyu Wang 已提交
328
  for (int32_t r = 0; r < rowNum; ++r) {
329
    void* rowData = taosArrayGetP(cols, r);
X
Xiaoyu Wang 已提交
330 331

    // 1. set the parsed value from sql string
332 333 334
    for (int c = 0; c < pTableCxt->boundColsInfo.numOfBound; ++c) {
      SSchema* pColSchema = &pSchema[pTableCxt->boundColsInfo.pColIndex[c]];
      SColVal* pVal = taosArrayGet(pTableCxt->pValues, pTableCxt->boundColsInfo.pColIndex[c]);
X
Xiaoyu Wang 已提交
335
      void**   p = taosHashGet(rowData, pColSchema->name, strlen(pColSchema->name));
336 337 338
      if (p == NULL) {
        continue;
      }
X
Xiaoyu Wang 已提交
339
      SSmlKv* kv = *(SSmlKv**)p;
wmmhello's avatar
wmmhello 已提交
340 341 342 343
      if(kv->type != pColSchema->type){
        ret = buildInvalidOperationMsg(&pBuf, "kv type not equal to col type");
        goto end;
      }
344 345 346
      if (pColSchema->type == TSDB_DATA_TYPE_TIMESTAMP) {
        kv->i = convertTimePrecision(kv->i, TSDB_TIME_PRECISION_NANO, pTableMeta->tableInfo.precision);
      }
347
      if (kv->type == TSDB_DATA_TYPE_NCHAR) {
348
        int32_t len = 0;
wmmhello's avatar
wmmhello 已提交
349
        char*   pUcs4 = taosMemoryCalloc(1, pColSchema->bytes - VARSTR_HEADER_SIZE);
350 351 352
        if (NULL == pUcs4) {
          ret = TSDB_CODE_OUT_OF_MEMORY;
          goto end;
X
Xiaoyu Wang 已提交
353
        }
wmmhello's avatar
wmmhello 已提交
354
        if (!taosMbsToUcs4(kv->value, kv->length, (TdUcs4*)pUcs4, pColSchema->bytes - VARSTR_HEADER_SIZE, &len)) {
355
          if (errno == E2BIG) {
D
dapan1121 已提交
356
            uError("sml bind taosMbsToUcs4 error, kv length:%d, bytes:%d, kv->value:%s", (int)kv->length, pColSchema->bytes, kv->value);
357 358 359 360 361 362
            buildInvalidOperationMsg(&pBuf, "value too long");
            ret = TSDB_CODE_PAR_VALUE_TOO_LONG;
            goto end;
          }
          ret = buildInvalidOperationMsg(&pBuf, strerror(errno));
          goto end;
X
Xiaoyu Wang 已提交
363
        }
364 365
        pVal->value.pData = pUcs4;
        pVal->value.nData = len;
366 367 368
      } else if (kv->type == TSDB_DATA_TYPE_BINARY) {
        pVal->value.nData = kv->length;
        pVal->value.pData = (uint8_t*)kv->value;
X
Xiaoyu Wang 已提交
369
      } else {
370
        memcpy(&pVal->value.val, &(kv->value), kv->length);
X
Xiaoyu Wang 已提交
371
      }
372
      pVal->flag = CV_FLAG_VALUE;
X
Xiaoyu Wang 已提交
373
    }
wmmhello's avatar
wmmhello 已提交
374 375 376 377 378 379 380 381

    SRow** pRow = taosArrayReserve(pTableCxt->pData->aRowP, 1);
    ret = tRowBuild(pTableCxt->pValues, pTableCxt->pSchema, pRow);
    if (TSDB_CODE_SUCCESS != ret) {
      buildInvalidOperationMsg(&pBuf, "tRowBuild error");
      goto end;
    }
    insCheckTableDataOrder(pTableCxt, TD_ROW_KEY(*pRow));
wmmhello's avatar
wmmhello 已提交
382
    clearColValArray(pTableCxt->pValues);
X
Xiaoyu Wang 已提交
383 384
  }

385
end:
X
Xiaoyu Wang 已提交
386
  insDestroyBoundColInfo(&bindTags);
wmmhello's avatar
wmmhello 已提交
387 388
  tdDestroySVCreateTbReq(pCreateTblReq);
  taosMemoryFree(pCreateTblReq);
389 390
  taosArrayDestroy(tagName);
  return ret;
X
Xiaoyu Wang 已提交
391 392
}

wmmhello's avatar
wmmhello 已提交
393
SQuery* smlInitHandle() {
394
  SQuery* pQuery = (SQuery*)nodesMakeNode(QUERY_NODE_QUERY);
wmmhello's avatar
wmmhello 已提交
395 396
  if (NULL == pQuery) {
    uError("create pQuery error");
397
    return NULL;
wmmhello's avatar
wmmhello 已提交
398 399 400 401
  }
  pQuery->execMode = QUERY_EXEC_MODE_SCHEDULE;
  pQuery->haveResultSet = false;
  pQuery->msgType = TDMT_VND_SUBMIT;
X
Xiaoyu Wang 已提交
402
  SVnodeModifyOpStmt* stmt = (SVnodeModifyOpStmt*)nodesMakeNode(QUERY_NODE_VNODE_MODIFY_STMT);
wmmhello's avatar
wmmhello 已提交
403
  if (NULL == stmt) {
X
Xiaoyu Wang 已提交
404
    uError("create SVnodeModifyOpStmt error");
wmmhello's avatar
wmmhello 已提交
405 406 407
    qDestroyQuery(pQuery);
    return NULL;
  }
408
  stmt->pTableBlockHashObj = taosHashInit(16, taosGetDefaultHashFunction(TSDB_DATA_TYPE_BIGINT), true, HASH_NO_LOCK);
wmmhello's avatar
wmmhello 已提交
409 410
  stmt->freeHashFunc = insDestroyTableDataCxtHashMap;
  stmt->freeArrayFunc = insDestroyVgroupDataCxtList;
X
Xiaoyu Wang 已提交
411

412
  pQuery->pRoot = (SNode*)stmt;
wmmhello's avatar
wmmhello 已提交
413
  return pQuery;
X
Xiaoyu Wang 已提交
414 415
}

416
int32_t smlBuildOutput(SQuery* handle, SHashObj* pVgHash) {
X
Xiaoyu Wang 已提交
417
  SVnodeModifyOpStmt* pStmt = (SVnodeModifyOpStmt*)(handle)->pRoot;
wmmhello's avatar
wmmhello 已提交
418 419 420 421 422 423 424 425 426 427 428 429
  // merge according to vgId
  int32_t code = insMergeTableDataCxt(pStmt->pTableBlockHashObj, &pStmt->pVgDataBlocks);
  if (code != TSDB_CODE_SUCCESS) {
    uError("insMergeTableDataCxt failed");
    return code;
  }
  code = insBuildVgDataBlocks(pVgHash, pStmt->pVgDataBlocks, &pStmt->pDataBlocks);
  if (code != TSDB_CODE_SUCCESS) {
    uError("insBuildVgDataBlocks failed");
    return code;
  }
  return code;
X
Xiaoyu Wang 已提交
430
}