/Users/deen/code/yugabyte-db/src/postgres/src/backend/executor/ybcExpr.c
Line | Count | Source (jump to first uncovered line) |
1 | | /*-------------------------------------------------------------------------------------------------- |
2 | | * ybcExpr.c |
3 | | * Routines to construct YBC expression tree. |
4 | | * |
5 | | * Copyright (c) YugaByte, Inc. |
6 | | * |
7 | | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
8 | | * in compliance with the License. You may obtain a copy of the License at |
9 | | * |
10 | | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | | * |
12 | | * Unless required by applicable law or agreed to in writing, software distributed under the License |
13 | | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
14 | | * or implied. See the License for the specific language governing permissions and limitations |
15 | | * under the License. |
16 | | * |
17 | | * IDENTIFICATION |
18 | | * src/backend/executor/ybcExpr.c |
19 | | *-------------------------------------------------------------------------------------------------- |
20 | | */ |
21 | | |
22 | | #include <inttypes.h> |
23 | | |
24 | | #include "postgres.h" |
25 | | |
26 | | #include "access/htup_details.h" |
27 | | #include "catalog/pg_collation.h" |
28 | | #include "catalog/pg_type.h" |
29 | | #include "catalog/pg_proc.h" |
30 | | #include "nodes/makefuncs.h" |
31 | | #include "nodes/nodeFuncs.h" |
32 | | #include "utils/datum.h" |
33 | | #include "utils/relcache.h" |
34 | | #include "utils/rel.h" |
35 | | #include "parser/parse_type.h" |
36 | | #include "utils/lsyscache.h" |
37 | | #include "commands/dbcommands.h" |
38 | | #include "executor/tuptable.h" |
39 | | #include "miscadmin.h" |
40 | | #include "utils/syscache.h" |
41 | | #include "utils/builtins.h" |
42 | | |
43 | | #include "pg_yb_utils.h" |
44 | | #include "executor/ybcExpr.h" |
45 | | #include "catalog/yb_type.h" |
46 | | |
47 | | Node *yb_expr_instantiate_params_mutator(Node *node, ParamListInfo paramLI); |
48 | | bool yb_pushdown_walker(Node *node, List **params); |
49 | | bool yb_can_pushdown_func(Oid funcid); |
50 | | |
51 | | YBCPgExpr YBCNewColumnRef(YBCPgStatement ybc_stmt, int16_t attr_num, |
52 | | int attr_typid, int attr_collation, |
53 | 7.55M | const YBCPgTypeAttrs *type_attrs) { |
54 | 7.55M | YBCPgExpr expr = NULL; |
55 | 7.55M | const YBCPgTypeEntity *type_entity = YbDataTypeFromOidMod(attr_num, attr_typid); |
56 | 7.55M | YBCPgCollationInfo collation_info; |
57 | 7.55M | YBGetCollationInfo(attr_collation, type_entity, 0 /* datum */, true /* is_null */, |
58 | 7.55M | &collation_info); |
59 | 7.55M | HandleYBStatus(YBCPgNewColumnRef(ybc_stmt, attr_num, type_entity, |
60 | 7.55M | collation_info.collate_is_valid_non_c, |
61 | 7.55M | type_attrs, &expr)); |
62 | 7.55M | return expr; |
63 | 7.55M | } |
64 | | |
65 | | YBCPgExpr YBCNewConstant(YBCPgStatement ybc_stmt, Oid type_id, Oid collation_id, |
66 | 11.2M | Datum datum, bool is_null) { |
67 | 11.2M | YBCPgExpr expr = NULL; |
68 | 11.2M | const YBCPgTypeEntity *type_entity = YbDataTypeFromOidMod(InvalidAttrNumber, type_id); |
69 | 11.2M | YBCPgCollationInfo collation_info; |
70 | 11.2M | YBGetCollationInfo(collation_id, type_entity, datum, is_null, &collation_info); |
71 | 11.2M | HandleYBStatus(YBCPgNewConstant(ybc_stmt, type_entity, |
72 | 11.2M | collation_info.collate_is_valid_non_c, |
73 | 11.2M | collation_info.sortkey, |
74 | 11.2M | datum, is_null, &expr)); |
75 | 11.2M | return expr; |
76 | 11.2M | } |
77 | | |
78 | 8 | YBCPgExpr YBCNewConstantVirtual(YBCPgStatement ybc_stmt, Oid type_id, YBCPgDatumKind kind) { |
79 | 8 | YBCPgExpr expr = NULL; |
80 | 8 | const YBCPgTypeEntity *type_entity = YbDataTypeFromOidMod(InvalidAttrNumber, type_id); |
81 | 8 | HandleYBStatus(YBCPgNewConstantVirtual(ybc_stmt, type_entity, kind, &expr)); |
82 | 8 | return expr; |
83 | 8 | } |
84 | | |
85 | | /* |
86 | | * yb_expr_instantiate_params_mutator |
87 | | * |
88 | | * Expression mutator used internally by YbExprInstantiateParams |
89 | | */ |
90 | | Node *yb_expr_instantiate_params_mutator(Node *node, ParamListInfo paramLI) |
91 | 0 | { |
92 | 0 | if (node == NULL) |
93 | 0 | return NULL; |
94 | | |
95 | 0 | if (IsA(node, Param)) |
96 | 0 | { |
97 | 0 | Param *param = castNode(Param, node); |
98 | 0 | ParamExternData *prm = NULL; |
99 | 0 | ParamExternData prmdata; |
100 | 0 | if (paramLI->paramFetch != NULL) |
101 | 0 | prm = paramLI->paramFetch(paramLI, param->paramid, |
102 | 0 | true, &prmdata); |
103 | 0 | else |
104 | 0 | prm = ¶mLI->params[param->paramid - 1]; |
105 | |
|
106 | 0 | if (!OidIsValid(prm->ptype) || |
107 | 0 | prm->ptype != param->paramtype) |
108 | 0 | { |
109 | | /* Planner should ensure this does not happen */ |
110 | 0 | elog(ERROR, "Invalid parameter: %s", nodeToString(param)); |
111 | 0 | } |
112 | 0 | int16 typLen = 0; |
113 | 0 | bool typByVal = false; |
114 | 0 | Datum pval = 0; |
115 | |
|
116 | 0 | get_typlenbyval(param->paramtype, &typLen, &typByVal); |
117 | 0 | if (prm->isnull || typByVal) |
118 | 0 | pval = prm->value; |
119 | 0 | else |
120 | 0 | pval = datumCopy(prm->value, typByVal, typLen); |
121 | |
|
122 | 0 | return (Node *) makeConst(param->paramtype, |
123 | 0 | param->paramtypmod, |
124 | 0 | param->paramcollid, |
125 | 0 | (int) typLen, |
126 | 0 | pval, |
127 | 0 | prm->isnull, |
128 | 0 | typByVal); |
129 | 0 | } |
130 | 0 | return expression_tree_mutator(node, |
131 | 0 | yb_expr_instantiate_params_mutator, |
132 | 0 | (void *) paramLI); |
133 | 0 | } |
134 | | |
135 | | /* |
136 | | * YbExprInstantiateParams |
137 | | * |
138 | | * Replace the Param nodes of the expression tree with Const nodes carrying |
139 | | * current parameter values before pushing the expression down to DocDB |
140 | | */ |
141 | | Expr *YbExprInstantiateParams(Expr* expr, ParamListInfo paramLI) |
142 | 22 | { |
143 | | /* Fast-path if there are no params. */ |
144 | 22 | if (paramLI == NULL) |
145 | 22 | return expr; |
146 | | |
147 | 0 | return (Expr *) expression_tree_mutator((Node *) expr, |
148 | 0 | yb_expr_instantiate_params_mutator, |
149 | 0 | (void *) paramLI); |
150 | 0 | } |
151 | | |
152 | | /* |
153 | | * yb_can_pushdown_func |
154 | | * |
155 | | * Determine if the function can be pushed down to DocDB |
156 | | * Since catalog access is not currently available in DocDB, only built in |
157 | | * functions are pushable. The lack of catalog access imposes also other |
158 | | * limitations: |
159 | | * - Only immutable functions are pushable. Stable and volatile functions |
160 | | * are permitted to access the catalog; |
161 | | * - DocDB must support conversion of parameter and result values between |
162 | | * DocDB and Postgres formats, so there should be conversion functions; |
163 | | * - Typically functions with polymorfic parameters or result need catalog |
164 | | * access to determine runtime data types, so they are not pushed down. |
165 | | */ |
166 | | bool yb_can_pushdown_func(Oid funcid) |
167 | 22 | { |
168 | 22 | HeapTuple tuple; |
169 | 22 | Form_pg_proc pg_proc; |
170 | 22 | bool result; |
171 | | |
172 | | /* Quick check if the function is builtin */ |
173 | 22 | if (!is_builtin_func(funcid)) |
174 | 0 | { |
175 | 0 | return false; |
176 | 0 | } |
177 | | |
178 | | /* |
179 | | * Check whether this function is on a list of hand-picked functions |
180 | | * safe for pushdown. |
181 | | */ |
182 | 44 | for (int i = 0; i < yb_funcs_safe_for_pushdown_count; ++i) |
183 | 22 | { |
184 | 22 | if (funcid == yb_funcs_safe_for_pushdown[i]) |
185 | 0 | return true; |
186 | 22 | } |
187 | | |
188 | | /* Examine misc function attributes that may affect pushability */ |
189 | 22 | tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); |
190 | 22 | if (!HeapTupleIsValid(tuple)) |
191 | 0 | elog(ERROR, "cache lookup failed for function %u", funcid); |
192 | 22 | pg_proc = (Form_pg_proc) GETSTRUCT(tuple); |
193 | 22 | result = true; |
194 | 22 | if (pg_proc->provolatile != PROVOLATILE_IMMUTABLE) |
195 | 0 | { |
196 | 0 | result = false; |
197 | 0 | } |
198 | 22 | if (result && |
199 | 22 | (!YBCPgFindTypeEntity(pg_proc->prorettype) || |
200 | 22 | IsPolymorphicType(pg_proc->prorettype))) |
201 | 0 | { |
202 | 0 | result = false; |
203 | 0 | } |
204 | 22 | if (result) |
205 | 22 | { |
206 | 66 | for (int i = 0; i < pg_proc->pronargs; i++) |
207 | 44 | { |
208 | 44 | Oid typid = pg_proc->proargtypes.values[i]; |
209 | 44 | if (!YBCPgFindTypeEntity(typid) || IsPolymorphicType(typid)) |
210 | 0 | { |
211 | 0 | result = false; |
212 | 0 | break; |
213 | 0 | } |
214 | 44 | } |
215 | 22 | } |
216 | 22 | ReleaseSysCache(tuple); |
217 | 22 | return result; |
218 | 22 | } |
219 | | |
220 | | /* |
221 | | * yb_pushdown_walker |
222 | | * |
223 | | * Expression walker used internally by YbCanPushdownExpr |
224 | | */ |
225 | | bool yb_pushdown_walker(Node *node, List **params) |
226 | 66 | { |
227 | 66 | if (node == NULL) |
228 | 0 | return false; |
229 | 66 | switch (node->type) |
230 | 66 | { |
231 | 22 | case T_Var: |
232 | 22 | { |
233 | 22 | Var *var_expr = castNode(Var, node); |
234 | 22 | AttrNumber attno = var_expr->varattno; |
235 | | /* DocDB is not aware of Postgres virtual attributes */ |
236 | 22 | if (!AttrNumberIsForUserDefinedAttr(attno)) |
237 | 0 | { |
238 | 0 | return true; |
239 | 0 | } |
240 | | /* Need to convert values between DocDB and Postgres formats */ |
241 | 22 | if (!YBCPgFindTypeEntity(var_expr->vartype)) |
242 | 0 | { |
243 | 0 | return true; |
244 | 0 | } |
245 | | /* Collect column reference */ |
246 | 22 | if (params) |
247 | 22 | { |
248 | 22 | ListCell *lc; |
249 | 22 | bool found = false; |
250 | | |
251 | | /* Check if the column reference has already been collected */ |
252 | 22 | foreach(lc, *params) |
253 | 0 | { |
254 | 0 | YbExprParamDesc *param = (YbExprParamDesc *) lfirst(lc); |
255 | 0 | if (param->attno == attno) |
256 | 0 | { |
257 | 0 | found = true; |
258 | 0 | break; |
259 | 0 | } |
260 | 0 | } |
261 | | |
262 | 22 | if (!found) |
263 | 22 | { |
264 | | /* Add new column reference to the list */ |
265 | 22 | YbExprParamDesc *new_param = makeNode(YbExprParamDesc); |
266 | 22 | new_param->attno = attno; |
267 | 22 | new_param->typid = var_expr->vartype; |
268 | 22 | new_param->typmod = var_expr->vartypmod; |
269 | 22 | new_param->collid = var_expr->varcollid; |
270 | 22 | *params = lappend(*params, new_param); |
271 | 22 | } |
272 | 22 | } |
273 | 22 | break; |
274 | 22 | } |
275 | 0 | case T_FuncExpr: |
276 | 0 | { |
277 | 0 | FuncExpr *func_expr = castNode(FuncExpr, node); |
278 | | /* DocDB executor does not expand variadic argument */ |
279 | 0 | if (func_expr->funcvariadic) |
280 | 0 | { |
281 | 0 | return true; |
282 | 0 | } |
283 | | /* Check if the function is pushable */ |
284 | 0 | if (!yb_can_pushdown_func(func_expr->funcid)) |
285 | 0 | { |
286 | 0 | return true; |
287 | 0 | } |
288 | 0 | break; |
289 | 0 | } |
290 | 22 | case T_OpExpr: |
291 | 22 | { |
292 | 22 | OpExpr *op_expr = castNode(OpExpr, node); |
293 | 22 | if (!yb_can_pushdown_func(op_expr->opfuncid)) |
294 | 0 | { |
295 | 0 | return true; |
296 | 0 | } |
297 | 22 | break; |
298 | 22 | } |
299 | 0 | case T_CaseExpr: |
300 | 0 | { |
301 | 0 | CaseExpr *case_expr = castNode(CaseExpr, node); |
302 | | /* |
303 | | * Support for implicit equality comparison would require catalog |
304 | | * lookup to find equality operation for the argument data type. |
305 | | */ |
306 | 0 | if (case_expr->arg) |
307 | 0 | { |
308 | 0 | return true; |
309 | 0 | } |
310 | 0 | break; |
311 | 0 | } |
312 | 0 | case T_Param: |
313 | 0 | { |
314 | 0 | Param *p = castNode(Param, node); |
315 | 0 | if (p->paramkind != PARAM_EXTERN || |
316 | 0 | !YBCPgFindTypeEntity(p->paramtype)) |
317 | 0 | { |
318 | 0 | return true; |
319 | 0 | } |
320 | 0 | break; |
321 | 0 | } |
322 | 22 | case T_Const: |
323 | 22 | { |
324 | 22 | Const *c = castNode(Const, node); |
325 | | /* |
326 | | * Constant value may need to be converted to DocDB format, but |
327 | | * DocDB does not support arbitrary types. |
328 | | */ |
329 | 22 | if (!YBCPgFindTypeEntity(c->consttype)) |
330 | 0 | { |
331 | 0 | return true; |
332 | 0 | } |
333 | 22 | break; |
334 | 22 | } |
335 | 0 | case T_RelabelType: |
336 | 0 | case T_NullTest: |
337 | 0 | case T_BoolExpr: |
338 | 0 | case T_CaseWhen: |
339 | 0 | break; |
340 | 0 | default: |
341 | 0 | return true; |
342 | 66 | } |
343 | 66 | return expression_tree_walker(node, yb_pushdown_walker, (void *) params); |
344 | 66 | } |
345 | | |
346 | | /* |
347 | | * YbCanPushdownExpr |
348 | | * |
349 | | * Determine if the expression is pushable. |
350 | | * In general, expression tree is pushable if DocDB knows how to execute |
351 | | * all its nodes, in other words, it should be handeled by the evalExpr() |
352 | | * function defined in the ybgate_api.c. In addition, external paremeter |
353 | | * references of supported data types are also pushable, since these |
354 | | * references are replaced with constants by YbExprInstantiateParams before |
355 | | * the DocDB request is sent. |
356 | | * |
357 | | * If the params parameter is provided, function also collects column |
358 | | * references represented by Var nodes in the expression tree. The params |
359 | | * list may be initially empty (NIL) or already contain some YbExprParamDesc |
360 | | * entries. That allows to collect column references from multiple |
361 | | * expressions into single list. The function avoids adding duplicate |
362 | | * references, however it does not remove duplecates if they are already |
363 | | * present in the params list. |
364 | | * |
365 | | * To add support for another expression node type it should be added to the |
366 | | * yb_pushdown_walker where it should check node attributes that may affect |
367 | | * pushability, and implement evaluation of that node type instance in the |
368 | | * evalExpr() function. |
369 | | */ |
370 | | bool YbCanPushdownExpr(Expr *pg_expr, List **params) |
371 | 28.4k | { |
372 | | /* respond with false if pushdown disabled in GUC */ |
373 | 28.4k | if (!yb_enable_expression_pushdown) |
374 | 28.4k | return false; |
375 | | |
376 | 22 | return !yb_pushdown_walker((Node *) pg_expr, params); |
377 | 22 | } |
378 | | |
379 | | /* |
380 | | * yb_transactional_walker |
381 | | * |
382 | | * Expression tree walker for the YbIsTransactionalExpr function. |
383 | | * As of initial version, it may be too optimistic, needs revisit. |
384 | | */ |
385 | | bool yb_transactional_walker(Node *node, void *context) |
386 | 1.04M | { |
387 | 1.04M | if (node == NULL) |
388 | 120k | return false; |
389 | 929k | switch (node->type) |
390 | 929k | { |
391 | 1.01k | case T_FuncExpr: |
392 | 1.01k | { |
393 | | /* |
394 | | * Built-in functions should be safe. If we learn of functions |
395 | | * that are unsafe we may need a blacklist here. |
396 | | * User defined function may be everything, unless it is immutable |
397 | | * By definition, immutable functions can not access database. |
398 | | * Otherwise safely assume that not immutable function would needs |
399 | | * a distributed transaction. |
400 | | */ |
401 | 1.01k | FuncExpr *func_expr = castNode(FuncExpr, node); |
402 | 1.01k | Oid funcid = func_expr->funcid; |
403 | 1.01k | HeapTuple tuple; |
404 | 1.01k | Form_pg_proc pg_proc; |
405 | 1.01k | if (is_builtin_func(funcid)) |
406 | 1.01k | { |
407 | 1.01k | break; |
408 | 1.01k | } |
409 | 0 | tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); |
410 | 0 | if (!HeapTupleIsValid(tuple)) |
411 | 0 | elog(ERROR, "cache lookup failed for function %u", funcid); |
412 | 0 | pg_proc = (Form_pg_proc) GETSTRUCT(tuple); |
413 | 0 | if (pg_proc->provolatile != PROVOLATILE_IMMUTABLE) |
414 | 0 | { |
415 | 0 | ReleaseSysCache(tuple); |
416 | 0 | return true; |
417 | 0 | } |
418 | 0 | ReleaseSysCache(tuple); |
419 | 0 | break; |
420 | 0 | } |
421 | | /* |
422 | | * The list of the expression types below was built by scrolling over |
423 | | * expression_tree_walker function and selecting those looking like they |
424 | | * do suspiciously transactional thing like running a subquery. |
425 | | */ |
426 | 2 | case T_NextValueExpr: |
427 | 2 | case T_RangeTblRef: |
428 | 2 | case T_SubLink: |
429 | 2 | case T_SubPlan: |
430 | 2 | case T_AlternativeSubPlan: |
431 | 2 | case T_Query: |
432 | 2 | case T_CommonTableExpr: |
433 | 2 | case T_FromExpr: |
434 | 2 | case T_JoinExpr: |
435 | 2 | case T_AppendRelInfo: |
436 | 2 | case T_RangeTblFunction: |
437 | 2 | case T_TableSampleClause: |
438 | 2 | case T_TableFunc: |
439 | 2 | return true; |
440 | | /* |
441 | | * Optimistically assume all other expression types do not |
442 | | * require a distributed transaction. |
443 | | */ |
444 | 928k | default: |
445 | 928k | break; |
446 | 929k | } |
447 | 929k | return expression_tree_walker(node, yb_transactional_walker, context); |
448 | 929k | } |
449 | | |
450 | | /* |
451 | | * YbIsTransactionalExpr |
452 | | * |
453 | | * Determine if the expression may need distributed transaction. |
454 | | * One shard modify table queries (INSERT, UPDATE, DELETE) running in |
455 | | * autocommit mode may skip starting distributed transactions. Without |
456 | | * distributed transaction overhead those statements perform much better. |
457 | | * However, certain expression may need to run a subquery or otherwise |
458 | | * access multiple nodes transactionally. This function checks if that |
459 | | * might be the case for given expression, and therefore distributed |
460 | | * transaction should be used for parent statement. |
461 | | * Historically the same function was used to determine if an expression |
462 | | * is pushable or if expression is (not) transactional, out of consideration |
463 | | * that if expression is simple enough to be pushable, it is not |
464 | | * transactional. That is generally true, pushable expression are not |
465 | | * transactional, however there are many not pushable expressions, which are |
466 | | * not transactional at the same time, so we can still benefit from higher |
467 | | * performing one shard queries even if they use not pushable expressions. |
468 | | * Besides, expression pushdown may be turned off with a GUC parameter. |
469 | | * If this function misdetermine transactional expression as not |
470 | | * transactional distributed transaction may be forced by surrounding the |
471 | | * statement with BEGIN; ... COMMIT; |
472 | | * Opposite misdetermination causes performance overhead only. |
473 | | */ |
474 | | bool YbIsTransactionalExpr(Node *pg_expr) |
475 | 239k | { |
476 | 239k | return yb_transactional_walker(pg_expr, NULL); |
477 | 239k | } |
478 | | |
479 | | /* |
480 | | * YBCNewEvalExprCall |
481 | | * |
482 | | * Serialize the Postgres expression tree and associate it with the |
483 | | * DocDB statement. Caller is supposed to ensure that expression is pushable |
484 | | * so DocDB can handle it. |
485 | | */ |
486 | | YBCPgExpr YBCNewEvalExprCall(YBCPgStatement ybc_stmt, Expr *pg_expr) |
487 | 5.76k | { |
488 | 5.76k | YBCPgExpr ybc_expr; |
489 | 5.76k | YBCPgCollationInfo collation_info; |
490 | 5.76k | const YBCPgTypeEntity *type_ent; |
491 | 5.76k | type_ent = YbDataTypeFromOidMod(InvalidAttrNumber, |
492 | 5.76k | exprType((Node *) pg_expr)); |
493 | 5.76k | YBGetCollationInfo(exprCollation((Node *) pg_expr), |
494 | 5.76k | type_ent, |
495 | 5.76k | 0 /* Datum */, |
496 | 5.76k | true /* is_null */, |
497 | 5.76k | &collation_info); |
498 | 5.76k | HandleYBStatus(YBCPgNewOperator(ybc_stmt, |
499 | 5.76k | "eval_expr_call", |
500 | 5.76k | type_ent, |
501 | 5.76k | collation_info.collate_is_valid_non_c, |
502 | 5.76k | &ybc_expr)); |
503 | | |
504 | 5.76k | Datum expr_datum = CStringGetDatum(nodeToString(pg_expr)); |
505 | 5.76k | YBCPgExpr expr = YBCNewConstant(ybc_stmt, CSTRINGOID, C_COLLATION_OID, |
506 | 5.76k | expr_datum , /* IsNull */ false); |
507 | 5.76k | HandleYBStatus(YBCPgOperatorAppendArg(ybc_expr, expr)); |
508 | 5.76k | return ybc_expr; |
509 | 5.76k | } |
510 | | |
511 | | /* ------------------------------------------------------------------------- */ |
512 | | /* Execution output parameter from Yugabyte */ |
513 | | YbPgExecOutParam *YbCreateExecOutParam() |
514 | 257 | { |
515 | 257 | YbPgExecOutParam *param = makeNode(YbPgExecOutParam); |
516 | 257 | param->bfoutput = makeStringInfo(); |
517 | | |
518 | | /* Not yet used */ |
519 | 257 | param->status = makeStringInfo(); |
520 | 257 | param->status_code = 0; |
521 | | |
522 | 257 | return param; |
523 | 257 | } |
524 | | |
525 | 257 | void YbWriteExecOutParam(YbPgExecOutParam *param, const YbcPgExecOutParamValue *value) { |
526 | 257 | appendStringInfoString(param->bfoutput, value->bfoutput); |
527 | | |
528 | | /* Not yet used */ |
529 | 257 | if (value->status) |
530 | 0 | { |
531 | 0 | appendStringInfoString(param->status, value->status); |
532 | 0 | param->status_code = value->status_code; |
533 | 0 | } |
534 | 257 | } |