Repository: taosdata/TDengine
Pull request #30625: feat: added base64 SQL function
Author: login: aestriplex
Labels: name: from community
Created: 2025-04-02T13:52:19Z | Merged: 2025-04-17T10:26:53Z

# Description

This PR adds the base64 SQL function for strings and byte arrays

此 PR 新增了字串和位元組數組的 base64 SQL 函數

# Checklist

- [x] Is the user manual updated?
- [x] Are the test cases passed and automated?
- [x] Is there no significant decrease in test coverage?



Diff:
diff --git a/docs/en/14-reference/03-taos-sql/10-function.md b/docs/en/14-reference/03-taos-sql/10-function.md
index e0286919275d..5be0a0d08285 100644
--- a/docs/en/14-reference/03-taos-sql/10-function.md
+++ b/docs/en/14-reference/03-taos-sql/10-function.md
@@ -1159,6 +1159,47 @@ taos> select repeat('abc',-1);
                   |
 ```
 
+#### TO_BASE64
+
+```sql
+TO_BASE64(expr)
+```
+
+**Function Description**: Returns the base64 encoding of the string `expr`s.
+
+**Return Type**: VARCHAR.
+
+**Applicable Data Types**:
+
+- `expr`: VARCHAR, NCHAR.
+
+**Nested Subquery Support**: Applicable to both inner and outer queries.
+
+**Applicable to**: Tables and supertables.
+
+**Usage Notes**:
+
+- If `expr` is NULL, returns NULL.
+
+**Example**:
+
+```sql
+taos> select to_base64("");
+ to_base64("") |
+================
+               |
+
+taos> select to_base64("Hello, world!");
+ to_base64("Hello, world!") |
+=============================
+ SGVsbG8sIHdvcmxkIQ==       |
+
+taos> select to_base64("你好 世界");
+ to_base64("你好 世界")      |
+==============================
+ 5L2g5aW9IOS4lueVjA==        |
+```
+
 ### Conversion Functions
 
 Conversion functions convert values from one data type to another.
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 e186f646a76b..409319aad52e 100644
--- a/docs/zh/14-reference/03-taos-sql/10-function.md
+++ b/docs/zh/14-reference/03-taos-sql/10-function.md
@@ -1110,6 +1110,47 @@ UPPER(expr)
 
 **适用于**：表和超级表。
 
+#### TO_BASE64
+
+```sql
+TO_BASE64(expr)
+```
+
+**功能说明**: 传回字符串“expr”的 Base64 编码。
+
+**返回结果类型**: VARCHAR
+
+**适用数据类型**:
+
+- `expr`: VARCHAR, NCHAR.
+
+**嵌套子查询支持**：适用于内层查询和外层查询。
+
+**适用于**：表和超级表。
+
+**使用说明**:
+
+- 若 expr 为 NULL，返回 NULL。
+
+**举例**:
+
+```sql
+taos> select to_base64("");
+ to_base64("") |
+================
+               |
+
+taos> select to_base64("Hello, world!");
+ to_base64("Hello, world!") |
+=============================
+ SGVsbG8sIHdvcmxkIQ==       |
+
+taos> select to_base64("你好 世界");
+ to_base64("你好 世界")      |
+==============================
+ 5L2g5aW9IOS4lueVjA==        |
+```
+
 ### 转换函数
 
 转换函数将值从一种数据类型转换为另一种数据类型。
diff --git a/include/libs/function/functionMgt.h b/include/libs/function/functionMgt.h
index b72aab16a6ca..b43e4fa22792 100644
--- a/include/libs/function/functionMgt.h
+++ b/include/libs/function/functionMgt.h
@@ -111,6 +111,7 @@ typedef enum EFunctionType {
   FUNCTION_TYPE_REPLACE,
   FUNCTION_TYPE_REPEAT,
   FUNCTION_TYPE_SUBSTR_IDX,
+  FUNCTION_TYPE_BASE64,
 
   // conversion function
   FUNCTION_TYPE_CAST = 2000,
diff --git a/include/libs/scalar/scalar.h b/include/libs/scalar/scalar.h
index 1941afd92db0..64427a2b0dc0 100644
--- a/include/libs/scalar/scalar.h
+++ b/include/libs/scalar/scalar.h
@@ -47,6 +47,7 @@ int32_t vectorGetConvertType(int32_t type1, int32_t type2);
 int32_t vectorConvertSingleColImpl(const SScalarParam *pIn, SScalarParam *pOut, int32_t *overflow, int32_t startIndex, int32_t numOfRows);
 int32_t vectorConvertSingleCol(SScalarParam *input, SScalarParam *output, int32_t type, STypeMod typeMod, int32_t startIndex, int32_t numOfRows);
 STypeMod getConvertTypeMod(int32_t type, const SColumnInfo *pCol1, const SColumnInfo *pCol2);
+uint32_t base64BufSize(size_t inputLenBytes);
 
 /* Math functions */
 int32_t absFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutput);
@@ -95,6 +96,7 @@ int32_t trimFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutp
 int32_t replaceFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutput);
 int32_t repeatFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutput);
 int32_t substrIdxFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutput);
+int32_t base64Function(SScalarParam* pInput, int32_t inputNum, SScalarParam* pOutput);
 
 /* Conversion functions */
 int32_t castFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *pOutput);
diff --git a/source/libs/function/src/builtins.c b/source/libs/function/src/builtins.c
index 2276c207b4e6..307acabdbc41 100644
--- a/source/libs/function/src/builtins.c
+++ b/source/libs/function/src/builtins.c
@@ -1010,6 +1010,16 @@ static int32_t translateOutDouble(SFunctionNode* pFunc, char* pErrBuf, int32_t l
   return TSDB_CODE_SUCCESS;
 }
 
+static int32_t translateBase64(SFunctionNode* pFunc, char* pErrBuf, int32_t len) {
+  FUNC_ERR_RET(validateParam(pFunc, pErrBuf, len));
+
+  SDataType* pRestType1 = getSDataTypeFromNode(nodesListGetNode(pFunc->pParameterList, 0));
+  int32_t outputLength = base64BufSize(pRestType1->bytes);
+
+  pFunc->node.resType = (SDataType){.bytes = outputLength, .type = TSDB_DATA_TYPE_VARCHAR};
+  return TSDB_CODE_SUCCESS;
+}
+
 static int32_t translateTrimStr(SFunctionNode* pFunc, char* pErrBuf, int32_t len, bool isLtrim) {
   FUNC_ERR_RET(validateParam(pFunc, pErrBuf, len));
 
@@ -5429,6 +5439,27 @@ const SBuiltinFuncDefinition funcMgtBuiltins[] = {
     .sprocessFunc = substrIdxFunction,
     .finalizeFunc = NULL
   },
+  {
+    .name = "to_base64",
+    .type = FUNCTION_TYPE_BASE64,
+    .classification = FUNC_MGT_SCALAR_FUNC | FUNC_MGT_STRING_FUNC,
+    .parameters = {.minParamNum = 1,
+                   .maxParamNum = 1,
+                   .paramInfoPattern = 1,
+                   .inputParaInfo[0][0] = {.isLastParam = true,
+                                           .startParam = 1,
+                                           .endParam = 1,
+                                           .validDataType = FUNC_PARAM_SUPPORT_VARCHAR_TYPE | FUNC_PARAM_SUPPORT_NCHAR_TYPE | FUNC_PARAM_SUPPORT_NULL_TYPE,
+                                           .validNodeType = FUNC_PARAM_SUPPORT_EXPR_NODE,
+                                           .paramAttribute = FUNC_PARAM_NO_SPECIFIC_ATTRIBUTE,
+                                           .valueRangeFlag = FUNC_PARAM_NO_SPECIFIC_VALUE,},
+                   .outputParaInfo = {.validDataType = FUNC_PARAM_SUPPORT_VARCHAR_TYPE}},
+    .translateFunc = translateBase64,
+    .getEnvFunc = NULL,
+    .initFunc = NULL,
+    .sprocessFunc = base64Function,
+    .finalizeFunc = NULL
+  },
   {
     .name = "char",
     .type = FUNCTION_TYPE_CHAR,
diff --git a/source/libs/scalar/src/sclfunc.c b/source/libs/scalar/src/sclfunc.c
index b0b110015193..387fc4390bad 100644
--- a/source/libs/scalar/src/sclfunc.c
+++ b/source/libs/scalar/src/sclfunc.c
@@ -1717,6 +1717,65 @@ int32_t substrIdxFunction(SScalarParam *pInput, int32_t inputNum, SScalarParam *
   return code;
 }
 
+static char base64Table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                            "abcdefghijklmnopqrstuvwxyz"
+                            "0123456789+/";
+
+static void base64Impl(uint8_t *base64Out, const uint8_t *inputBytes, size_t inputLen, VarDataLenT outputLen) {
+  for (size_t i = 0, j = 0; i < inputLen;) {
+    unsigned int octet_a = i < inputLen ? inputBytes[i++] : 0;
+    unsigned int octet_b = i < inputLen ? inputBytes[i++] : 0;
+    unsigned int octet_c = i < inputLen ? inputBytes[i++] : 0;
+
+    unsigned int triple = (octet_a << 16) | (octet_b << 8) | octet_c;
+
+    base64Out[j++] = base64Table[(triple >> 18) & 0x3F];
+    base64Out[j++] = base64Table[(triple >> 12) & 0x3F];
+    base64Out[j++] = base64Table[(triple >> 6) & 0x3F];
+    base64Out[j++] = base64Table[triple & 0x3F];
+  }
+
+  for (int k = 0; k < (3 - (inputLen % 3)) % 3; k++) {
+    base64Out[outputLen - k - 1] = '=';
+  }
+
+  base64Out[outputLen] = 0;
+}
+
+uint32_t base64BufSize(size_t inputLenBytes) {
+  return 4 * ((inputLenBytes + 2) / 3);
+}
+
+int32_t base64Function(SScalarParam* pInput, int32_t inputNum, SScalarParam* pOutput) {
+  int32_t code = TSDB_CODE_SUCCESS;
+  SColumnInfoData *pInputData = pInput->columnData;
+  SColumnInfoData *pOutputData = pOutput->columnData;
+  char *outputBuf = taosMemoryMalloc(TSDB_MAX_FIELD_LEN + VARSTR_HEADER_SIZE);
+  if (outputBuf == NULL) {
+    SCL_ERR_RET(terrno);
+  }
+  
+  for (int32_t i = 0; i < pInput->numOfRows; ++i) {
+    if (colDataIsNull_s(pInputData, i)) {
+      colDataSetNULL(pOutputData, i);
+      continue;
+    }
+
+    char *input = colDataGetData(pInputData, i);
+    size_t inputLen = varDataLen(colDataGetData(pInputData, i));
+    char *out = outputBuf + VARSTR_HEADER_SIZE;
+    VarDataLenT outputLength = base64BufSize(inputLen);
+    base64Impl(out, varDataVal(input), inputLen, outputLength);
+    varDataSetLen(outputBuf, outputLength);
+    SCL_ERR_JRET(colDataSetVal(pOutputData, i, outputBuf, false));
+  }
+
+  pOutput->numOfRows = pInput->numOfRows;
+_return:
+  taosMemoryFree(outputBuf);
+  return code;
+}
+
 static int32_t repeatStringHelper(char *input, int32_t inputLen, int32_t count, char *output) {
   for (int32_t i = 0; i < count; ++i) {
     (void)memcpy(output, input, inputLen);
diff --git a/tests/army/query/function/ans/base64.csv b/tests/army/query/function/ans/base64.csv
new file mode 100644
index 000000000000..8ce1b1e9656e
--- /dev/null
+++ b/tests/army/query/function/ans/base64.csv
@@ -0,0 +1,71 @@
+
+taos> select to_base64(NULL)
+ to_base64(null) |
+==================
+ NULL            |
+
+taos> select to_base64('')
+ to_base64('') |
+================
+               |
+
+taos> select to_base64('aaaa')
+ to_base64('aaaa') |
+====================
+ YWFhYQ==          |
+
+taos> select to_base64('Hello, world!')
+ to_base64('Hello, world!') |
+=============================
+ SGVsbG8sIHdvcmxkIQ==       |
+
+taos> select to_base64(concat(lower(concat('THIS',' ','IS',' ','A',' ')),'test'))
+ to_base64(concat(lower(concat('THIS',' ','IS',' ','A',' ')),'tes |
+===================================================================
+ dGhpcyBpcyBhIHRlc3Q=                                             |
+
+taos> select to_base64('北京涛思')
+ to_base64('北京涛思') |
+============================
+ 5YyX5Lqs5rab5oCd          |
+
+taos> select to_base64('北京涛思.com')
+ to_base64('北京涛思.com') |
+================================
+ 5YyX5Lqs5rab5oCdLmNvbQ==      |
+
+taos> select to_base64(repeat('Hello, world!', 5))
+ to_base64(repeat('Hello, world!', 5)) |
+========================================
+ SGVsbG8sIHdvcmxkIUhlbGxvLCB3b3JsZC... |
+
+taos> select to_base64(repeat('北京涛思.com', 5))
+ to_base64(repeat('北京涛思.com', 5)) |
+===========================================
+ 5YyX5Lqs5rab5oCdLmNvbeWMl+S6rOa2m+aAn... |
+
+taos> select to_base64(trim('  Hello, world!  '))
+ to_base64(trim('  Hello, world!  ')) |
+=======================================
+ SGVsbG8sIHdvcmxkIQ==                 |
+
+taos> select to_base64(trim('  Hello, world!  ')) == to_base64('Hello, world!')
+ to_base64(trim('  Hello, world!  ')) == to_base64('Hello, world! |
+===================================================================
+ true                                                             |
+
+taos> select to_base64(trim('  北京涛思  '))
+ to_base64(trim('  北京涛思  ')) |
+======================================
+ 5YyX5Lqs5rab5oCd                    |
+
+taos> select to_base64(trim('  北京涛思  ')) == to_base64('北京涛思')
+ to_base64(trim('  北京涛思  ')) == to_base64('北京涛思') |
+===================================================================
+ true                                                             |
+
+taos> select to_base64(substring('北京涛思.com', 1, 4)) == to_base64('北京涛思')
+ to_base64(substring('北京涛思.com', 1, 4)) == to_base64('北 |
+===================================================================
+ true                                                             |
+
diff --git a/tests/army/query/function/in/base64.err b/tests/army/query/function/in/base64.err
new file mode 100644
index 000000000000..1a40b210a6d3
--- /dev/null
+++ b/tests/army/query/function/in/base64.err
@@ -0,0 +1,4 @@
+select to_base64();
+select to_base64(123);
+select to_base64(123.45);
+SELECT to_base64(cast("123" as INTEGER));
diff --git a/tests/army/query/function/in/base64.in b/tests/army/query/function/in/base64.in
new file mode 100644
index 000000000000..d6586021225f
--- /dev/null
+++ b/tests/army/query/function/in/base64.in
@@ -0,0 +1,14 @@
+select to_base64(NULL)
+select to_base64('')
+select to_base64('aaaa')
+select to_base64('Hello, world!')
+select to_base64(concat(lower(concat('THIS',' ','IS',' ','A',' ')),'test'))
+select to_base64('北京涛思')
+select to_base64('北京涛思.com')
+select to_base64(repeat('Hello, world!', 5))
+select to_base64(repeat('北京涛思.com', 5))
+select to_base64(trim('  Hello, world!  '))
+select to_base64(trim('  Hello, world!  ')) == to_base64('Hello, world!')
+select to_base64(trim('  北京涛思  '))
+select to_base64(trim('  北京涛思  ')) == to_base64('北京涛思')
+select to_base64(substring('北京涛思.com', 1, 4)) == to_base64('北京涛思')
diff --git a/tests/army/query/function/test_function.py b/tests/army/query/function/test_function.py
index 3b84583b2069..670f2199612e 100644
--- a/tests/army/query/function/test_function.py
+++ b/tests/army/query/function/test_function.py
@@ -147,6 +147,9 @@ def test_substr_idx(self):
 
     def test_trim(self):
         self.test_normal_query_new("trim")
+        
+    def test_base64(self):
+        self.test_normal_query_new("base64")
 
     def test_timediff(self):
         self.test_normal_query_new("timediff")
@@ -530,6 +533,7 @@ def run(self):
         self.test_substr()
         self.test_substr_idx()
         self.test_trim()
+        self.test_base64()
 
         # time function
         self.test_timediff()


Reviews:
user: login: dapan1121
body: The PR should be applied to branch 3.0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T05:56:34Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T06:12:42Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T06:13:10Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T07:13:59Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:18:49Z
user: login: aestriplex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:38:02Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:19:09Z
user: login: dapan1121
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T06:42:12Z
user: login: Simon9997
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T10:02:01Z
user: login: dapan1121
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T10:26:43Z

Review comments:
diff_hunk: @@ -1110,6 +1110,47 @@ UPPER(expr)
 
 **适用于**：表和超级表。
 
+#### BASE64
+
+```sql
+BASE64(expr)
+```
+
+**功能说明**: 傳回字串“expr”的 Base64 編碼。
path: docs/zh/14-reference/03-taos-sql/10-function.md
user: login: Simon9997
body: use simplified Chinese here
**功能说明**: 传回字符串“expr”的 Base64 编码。
created_at: 2025-04-07T06:12:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1110,6 +1110,47 @@ UPPER(expr)
 
 **适用于**：表和超级表。
 
+#### BASE64
+
+```sql
+BASE64(expr)
+```
+
+**功能说明**: 傳回字串“expr”的 Base64 編碼。
+
+**返回结果类型**: 與輸入欄位“expr”的原始類型相同。
path: docs/zh/14-reference/03-taos-sql/10-function.md
user: login: Simon9997
body: use simplified Chinese
**返回结果类型**: 与 `expr` 字段的原始数据类型一致。
created_at: 2025-04-07T06:13:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1010,6 +1010,16 @@ static int32_t translateOutDouble(SFunctionNode* pFunc, char* pErrBuf, int32_t l
   return TSDB_CODE_SUCCESS;
 }
 
+static int32_t translateBase64(SFunctionNode* pFunc, char* pErrBuf, int32_t len) {
+  FUNC_ERR_RET(validateParam(pFunc, pErrBuf, len));
+
+  SDataType* pRestType1 = getSDataTypeFromNode(nodesListGetNode(pFunc->pParameterList, 0));
+  int32_t outputLength = base64BufSize(pRestType1->bytes);
+
+  pFunc->node.resType = (SDataType){.bytes = outputLength, .type = TSDB_DATA_TYPE_VARCHAR};
path: source/libs/function/src/builtins.c
user: login: Simon9997
body: if the return type of base64 is same to the input parameter's type, .type 's value should be input parameter's type, not fixed type TSDB_DATA_TYPE_VARCHAR
created_at: 2025-04-07T07:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1159,6 +1159,47 @@ taos> select repeat('abc',-1);
                   |
 ```
 
+#### BASE64
+
+```sql
+BASE64(expr)
+```
+
+**Function Description**: Returns the base64 encoding of the string `expr`s.
+
+**Return Type**: Same as the original type of the input field `expr`.
path: docs/en/14-reference/03-taos-sql/10-function.md
user: login: Simon9997
body: why the return type is same of `expr`? Why not just return VARCHAR type
created_at: 2025-04-10T00:18:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1159,6 +1159,47 @@ taos> select repeat('abc',-1);
                   |
 ```
 
+#### BASE64
+
+```sql
+BASE64(expr)
+```
+
+**Function Description**: Returns the base64 encoding of the string `expr`s.
+
+**Return Type**: Same as the original type of the input field `expr`.
path: docs/en/14-reference/03-taos-sql/10-function.md
user: login: aestriplex
body: If I change this one, should I change [this one](https://github.com/taosdata/TDengine/pull/30625#discussion_r2030592091) back again?
created_at: 2025-04-10T08:38:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1159,6 +1159,47 @@ taos> select repeat('abc',-1);
                   |
 ```
 
+#### BASE64
+
+```sql
+BASE64(expr)
+```
+
+**Function Description**: Returns the base64 encoding of the string `expr`s.
+
+**Return Type**: Same as the original type of the input field `expr`.
path: docs/en/14-reference/03-taos-sql/10-function.md
user: login: Simon9997
body: yes
created_at: 2025-04-10T10:19:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1159,6 +1159,47 @@ taos> select repeat('abc',-1);
                   |
 ```
 
+#### BASE64
+
+```sql
+BASE64(expr)
path: docs/en/14-reference/03-taos-sql/10-function.md
user: login: Simon9997
body: this should be to_base64, same to mysql's function
created_at: 2025-04-15T10:02:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: tomchon
created_at: 2025-04-08T07:29:07Z
body: @aestriplex  Just a gentle reminder that merging the 3.0 branch again might be needed to trigger the CI workflow for merging into 3.0. Thank you!
author_association: CONTRIBUTOR
user: login: aestriplex
created_at: 2025-04-08T11:50:42Z
body: > @aestriplex Just a gentle reminder that merging the 3.0 branch again might be needed to trigger the CI workflow for merging into 3.0. Thank you!

@tomchon (cc. @Simon9997) Apparently there is still a test that fails
```
./test.sh -f tsim/query/udfpy.sim
```
but now my branch is one exact commit ahead of upstream/3.0
 
![Screenshot](https://github.com/user-attachments/assets/ee0b1539-9d39-4a22-b3bc-d005d457c2ce)

author_association: CONTRIBUTOR
user: login: tomchon
created_at: 2025-04-08T12:17:21Z
body: @aestriplex   This issue is not related to this PR. It is already being tracked and resolved internally.


> > @aestriplex Just a gentle reminder that merging the 3.0 branch again might be needed to trigger the CI workflow for merging into 3.0. Thank you!
> 
> @tomchon (cc. @Simon9997) Apparently there is still a test that fails
> 
> ```
> ./test.sh -f tsim/query/udfpy.sim
> ```
> 
> but now my branch is one exact commit ahead of upstream/3.0
> 
> ![Screenshot](https://private-user-images.githubusercontent.com/46693538/431356657-ee0b1539-9d39-4a22-b3bc-d005d457c2ce.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQxMTQ4MDcsIm5iZiI6MTc0NDExNDUwNywicGF0aCI6Ii80NjY5MzUzOC80MzEzNTY2NTctZWUwYjE1MzktOWQzOS00YTIyLWIzYmMtZDAwNWQ0NTdjMmNlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA0MDglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNDA4VDEyMTUwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTVkYWM2ZTY1MTRiOGU2ZWE1NTNmYjYyOWU5YTdhMWI2NjkyNTBmNDM5MGU5YmJlNTQ0ODJkOTIyMmJjNDQyOWImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.BleC0ALQLyFGxl8q7qBeVpSazyqEDvJIuW9R3v_0L4A)


author_association: CONTRIBUTOR