Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * aggregatecmds.c
4 : : *
5 : : * Routines for aggregate-manipulation commands
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : *
11 : : * IDENTIFICATION
12 : : * src/backend/commands/aggregatecmds.c
13 : : *
14 : : * DESCRIPTION
15 : : * The "DefineAggregate" routine takes the parse tree and picks out the
16 : : * appropriate arguments/flags, passing the results to the
17 : : * "AggregateCreate" routine (in src/backend/catalog), which does the
18 : : * actual catalog-munging. DefineAggregate also verifies the permission of
19 : : * the user to execute the command.
20 : : *
21 : : *-------------------------------------------------------------------------
22 : : */
23 : : #include "postgres.h"
24 : :
25 : : #include "catalog/namespace.h"
26 : : #include "catalog/pg_aggregate.h"
27 : : #include "catalog/pg_namespace.h"
28 : : #include "catalog/pg_proc.h"
29 : : #include "catalog/pg_type.h"
30 : : #include "commands/defrem.h"
31 : : #include "miscadmin.h"
32 : : #include "parser/parse_type.h"
33 : : #include "utils/acl.h"
34 : : #include "utils/builtins.h"
35 : : #include "utils/lsyscache.h"
36 : :
37 : :
38 : : static char extractModify(DefElem *defel);
39 : :
40 : :
41 : : /*
42 : : * DefineAggregate
43 : : *
44 : : * "oldstyle" signals the old (pre-8.2) style where the aggregate input type
45 : : * is specified by a BASETYPE element in the parameters. Otherwise,
46 : : * "args" is a pair, whose first element is a list of FunctionParameter structs
47 : : * defining the agg's arguments (both direct and aggregated), and whose second
48 : : * element is an Integer node with the number of direct args, or -1 if this
49 : : * isn't an ordered-set aggregate.
50 : : * "parameters" is a list of DefElem representing the agg's definition clauses.
51 : : */
52 : : ObjectAddress
53 : 585 : DefineAggregate(ParseState *pstate,
54 : : List *name,
55 : : List *args,
56 : : bool oldstyle,
57 : : List *parameters,
58 : : bool replace)
59 : : {
60 : : char *aggName;
61 : : Oid aggNamespace;
62 : : AclResult aclresult;
63 : 585 : char aggKind = AGGKIND_NORMAL;
64 : 585 : List *transfuncName = NIL;
65 : 585 : List *finalfuncName = NIL;
66 : 585 : List *combinefuncName = NIL;
67 : 585 : List *serialfuncName = NIL;
68 : 585 : List *deserialfuncName = NIL;
69 : 585 : List *mtransfuncName = NIL;
70 : 585 : List *minvtransfuncName = NIL;
71 : 585 : List *mfinalfuncName = NIL;
72 : 585 : bool finalfuncExtraArgs = false;
73 : 585 : bool mfinalfuncExtraArgs = false;
74 : 585 : char finalfuncModify = 0;
75 : 585 : char mfinalfuncModify = 0;
76 : 585 : List *sortoperatorName = NIL;
77 : 585 : List *supportfuncName = NIL;
78 : 585 : TypeName *baseType = NULL;
79 : 585 : TypeName *transType = NULL;
80 : 585 : TypeName *mtransType = NULL;
81 : 585 : int32 transSpace = 0;
82 : 585 : int32 mtransSpace = 0;
83 : 585 : char *initval = NULL;
84 : 585 : char *minitval = NULL;
85 : 585 : char *parallel = NULL;
86 : : int numArgs;
87 : 585 : int numDirectArgs = 0;
88 : : oidvector *parameterTypes;
89 : : ArrayType *allParameterTypes;
90 : : ArrayType *parameterModes;
91 : : ArrayType *parameterNames;
92 : : List *parameterDefaults;
93 : : Oid variadicArgType;
94 : : Oid transTypeId;
95 : 585 : Oid mtransTypeId = InvalidOid;
96 : : char transTypeType;
97 : 585 : char mtransTypeType = 0;
98 : 585 : char proparallel = PROPARALLEL_UNSAFE;
99 : : ListCell *pl;
100 : :
101 : : /* Convert list of names to a name and namespace */
102 : 585 : aggNamespace = QualifiedNameGetCreationNamespace(name, &aggName);
103 : :
104 : : /* Check we have creation rights in target namespace */
105 : 585 : aclresult = object_aclcheck(NamespaceRelationId, aggNamespace, GetUserId(), ACL_CREATE);
106 [ - + ]: 585 : if (aclresult != ACLCHECK_OK)
107 : 0 : aclcheck_error(aclresult, OBJECT_SCHEMA,
108 : 0 : get_namespace_name(aggNamespace));
109 : :
110 : : /* Deconstruct the output of the aggr_args grammar production */
111 [ + + ]: 585 : if (!oldstyle)
112 : : {
113 : : Assert(list_length(args) == 2);
114 : 345 : numDirectArgs = intVal(lsecond(args));
115 [ + + ]: 345 : if (numDirectArgs >= 0)
116 : 14 : aggKind = AGGKIND_ORDERED_SET;
117 : : else
118 : 331 : numDirectArgs = 0;
119 : 345 : args = linitial_node(List, args);
120 : : }
121 : :
122 : : /* Examine aggregate's definition clauses */
123 [ + - + + : 2892 : foreach(pl, parameters)
+ + ]
124 : : {
125 : 2307 : DefElem *defel = lfirst_node(DefElem, pl);
126 : :
127 : : /*
128 : : * sfunc1, stype1, and initcond1 are accepted as obsolete spellings
129 : : * for sfunc, stype, initcond.
130 : : */
131 [ + + ]: 2307 : if (strcmp(defel->defname, "sfunc") == 0)
132 : 557 : transfuncName = defGetQualifiedName(defel);
133 [ + + ]: 1750 : else if (strcmp(defel->defname, "sfunc1") == 0)
134 : 20 : transfuncName = defGetQualifiedName(defel);
135 [ + + ]: 1730 : else if (strcmp(defel->defname, "finalfunc") == 0)
136 : 251 : finalfuncName = defGetQualifiedName(defel);
137 [ + + ]: 1479 : else if (strcmp(defel->defname, "combinefunc") == 0)
138 : 20 : combinefuncName = defGetQualifiedName(defel);
139 [ + + ]: 1459 : else if (strcmp(defel->defname, "serialfunc") == 0)
140 : 24 : serialfuncName = defGetQualifiedName(defel);
141 [ + + ]: 1435 : else if (strcmp(defel->defname, "deserialfunc") == 0)
142 : 20 : deserialfuncName = defGetQualifiedName(defel);
143 [ + + ]: 1415 : else if (strcmp(defel->defname, "msfunc") == 0)
144 : 38 : mtransfuncName = defGetQualifiedName(defel);
145 [ + + ]: 1377 : else if (strcmp(defel->defname, "minvfunc") == 0)
146 : 38 : minvtransfuncName = defGetQualifiedName(defel);
147 [ - + ]: 1339 : else if (strcmp(defel->defname, "mfinalfunc") == 0)
148 : 0 : mfinalfuncName = defGetQualifiedName(defel);
149 [ + + ]: 1339 : else if (strcmp(defel->defname, "finalfunc_extra") == 0)
150 : 10 : finalfuncExtraArgs = defGetBoolean(defel);
151 [ - + ]: 1329 : else if (strcmp(defel->defname, "mfinalfunc_extra") == 0)
152 : 0 : mfinalfuncExtraArgs = defGetBoolean(defel);
153 [ + + ]: 1329 : else if (strcmp(defel->defname, "finalfunc_modify") == 0)
154 : 13 : finalfuncModify = extractModify(defel);
155 [ - + ]: 1316 : else if (strcmp(defel->defname, "mfinalfunc_modify") == 0)
156 : 0 : mfinalfuncModify = extractModify(defel);
157 [ + + ]: 1316 : else if (strcmp(defel->defname, "sortop") == 0)
158 : 4 : sortoperatorName = defGetQualifiedName(defel);
159 [ + + ]: 1312 : else if (strcmp(defel->defname, "support") == 0)
160 : 5 : supportfuncName = defGetQualifiedName(defel);
161 [ + + ]: 1307 : else if (strcmp(defel->defname, "basetype") == 0)
162 : 232 : baseType = defGetTypeName(defel);
163 [ + + ]: 1075 : else if (strcmp(defel->defname, "hypothetical") == 0)
164 : : {
165 [ + - ]: 5 : if (defGetBoolean(defel))
166 : : {
167 [ - + ]: 5 : if (aggKind == AGGKIND_NORMAL)
168 [ # # ]: 0 : ereport(ERROR,
169 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
170 : : errmsg("only ordered-set aggregates can be hypothetical")));
171 : 5 : aggKind = AGGKIND_HYPOTHETICAL;
172 : : }
173 : : }
174 [ + + ]: 1070 : else if (strcmp(defel->defname, "stype") == 0)
175 : 557 : transType = defGetTypeName(defel);
176 [ + + ]: 513 : else if (strcmp(defel->defname, "stype1") == 0)
177 : 20 : transType = defGetTypeName(defel);
178 [ + + ]: 493 : else if (strcmp(defel->defname, "sspace") == 0)
179 : 5 : transSpace = defGetInt32(defel);
180 [ + + ]: 488 : else if (strcmp(defel->defname, "mstype") == 0)
181 : 38 : mtransType = defGetTypeName(defel);
182 [ - + ]: 450 : else if (strcmp(defel->defname, "msspace") == 0)
183 : 0 : mtransSpace = defGetInt32(defel);
184 [ + + ]: 450 : else if (strcmp(defel->defname, "initcond") == 0)
185 : 364 : initval = defGetString(defel);
186 [ + + ]: 86 : else if (strcmp(defel->defname, "initcond1") == 0)
187 : 11 : initval = defGetString(defel);
188 [ + + ]: 75 : else if (strcmp(defel->defname, "minitcond") == 0)
189 : 10 : minitval = defGetString(defel);
190 [ + + ]: 65 : else if (strcmp(defel->defname, "parallel") == 0)
191 : 21 : parallel = defGetString(defel);
192 : : else
193 [ + - ]: 44 : ereport(WARNING,
194 : : (errcode(ERRCODE_SYNTAX_ERROR),
195 : : errmsg("aggregate attribute \"%s\" not recognized",
196 : : defel->defname)));
197 : : }
198 : :
199 : : /*
200 : : * make sure we have our required definitions
201 : : */
202 [ + + ]: 585 : if (transType == NULL)
203 [ + - ]: 8 : ereport(ERROR,
204 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
205 : : errmsg("aggregate stype must be specified")));
206 [ - + ]: 577 : if (transfuncName == NIL)
207 [ # # ]: 0 : ereport(ERROR,
208 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
209 : : errmsg("aggregate sfunc must be specified")));
210 : :
211 : : /*
212 : : * if mtransType is given, mtransfuncName and minvtransfuncName must be as
213 : : * well; if not, then none of the moving-aggregate options should have
214 : : * been given.
215 : : */
216 [ + + ]: 577 : if (mtransType != NULL)
217 : : {
218 [ - + ]: 38 : if (mtransfuncName == NIL)
219 [ # # ]: 0 : ereport(ERROR,
220 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
221 : : errmsg("aggregate msfunc must be specified when mstype is specified")));
222 [ - + ]: 38 : if (minvtransfuncName == NIL)
223 [ # # ]: 0 : ereport(ERROR,
224 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
225 : : errmsg("aggregate minvfunc must be specified when mstype is specified")));
226 : : }
227 : : else
228 : : {
229 [ - + ]: 539 : if (mtransfuncName != NIL)
230 [ # # ]: 0 : ereport(ERROR,
231 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
232 : : errmsg("aggregate msfunc must not be specified without mstype")));
233 [ - + ]: 539 : if (minvtransfuncName != NIL)
234 [ # # ]: 0 : ereport(ERROR,
235 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
236 : : errmsg("aggregate minvfunc must not be specified without mstype")));
237 [ - + ]: 539 : if (mfinalfuncName != NIL)
238 [ # # ]: 0 : ereport(ERROR,
239 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
240 : : errmsg("aggregate mfinalfunc must not be specified without mstype")));
241 [ - + ]: 539 : if (mtransSpace != 0)
242 [ # # ]: 0 : ereport(ERROR,
243 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
244 : : errmsg("aggregate msspace must not be specified without mstype")));
245 [ - + ]: 539 : if (minitval != NULL)
246 [ # # ]: 0 : ereport(ERROR,
247 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
248 : : errmsg("aggregate minitcond must not be specified without mstype")));
249 : : }
250 : :
251 : : /*
252 : : * Default values for modify flags can only be determined once we know the
253 : : * aggKind.
254 : : */
255 [ + + ]: 577 : if (finalfuncModify == 0)
256 [ + + ]: 564 : finalfuncModify = (aggKind == AGGKIND_NORMAL) ? AGGMODIFY_READ_ONLY : AGGMODIFY_READ_WRITE;
257 [ + - ]: 577 : if (mfinalfuncModify == 0)
258 [ + + ]: 577 : mfinalfuncModify = (aggKind == AGGKIND_NORMAL) ? AGGMODIFY_READ_ONLY : AGGMODIFY_READ_WRITE;
259 : :
260 : : /*
261 : : * look up the aggregate's input datatype(s).
262 : : */
263 [ + + ]: 577 : if (oldstyle)
264 : : {
265 : : /*
266 : : * Old style: use basetype parameter. This supports aggregates of
267 : : * zero or one input, with input type ANY meaning zero inputs.
268 : : *
269 : : * Historically we allowed the command to look like basetype = 'ANY'
270 : : * so we must do a case-insensitive comparison for the name ANY. Ugh.
271 : : */
272 : : Oid aggArgTypes[1];
273 : :
274 [ + + ]: 236 : if (baseType == NULL)
275 [ + - ]: 4 : ereport(ERROR,
276 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
277 : : errmsg("aggregate input type must be specified")));
278 : :
279 [ + + ]: 232 : if (pg_strcasecmp(TypeNameToString(baseType), "ANY") == 0)
280 : : {
281 : 4 : numArgs = 0;
282 : 4 : aggArgTypes[0] = InvalidOid;
283 : : }
284 : : else
285 : : {
286 : 228 : numArgs = 1;
287 : 228 : aggArgTypes[0] = typenameTypeId(NULL, baseType);
288 : : }
289 : 232 : parameterTypes = buildoidvector(aggArgTypes, numArgs);
290 : 232 : allParameterTypes = NULL;
291 : 232 : parameterModes = NULL;
292 : 232 : parameterNames = NULL;
293 : 232 : parameterDefaults = NIL;
294 : 232 : variadicArgType = InvalidOid;
295 : : }
296 : : else
297 : : {
298 : : /*
299 : : * New style: args is a list of FunctionParameters (possibly zero of
300 : : * 'em). We share functioncmds.c's code for processing them.
301 : : */
302 : : Oid requiredResultType;
303 : :
304 [ - + ]: 341 : if (baseType != NULL)
305 [ # # ]: 0 : ereport(ERROR,
306 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
307 : : errmsg("basetype is redundant with aggregate input type specification")));
308 : :
309 : 341 : numArgs = list_length(args);
310 : 341 : interpret_function_parameter_list(pstate,
311 : : args,
312 : : InvalidOid,
313 : : OBJECT_AGGREGATE,
314 : : ¶meterTypes,
315 : : NULL,
316 : : &allParameterTypes,
317 : : ¶meterModes,
318 : : ¶meterNames,
319 : : NULL,
320 : : ¶meterDefaults,
321 : : &variadicArgType,
322 : : &requiredResultType);
323 : : /* Parameter defaults are not currently allowed by the grammar */
324 : : Assert(parameterDefaults == NIL);
325 : : /* There shouldn't have been any OUT parameters, either */
326 : : Assert(requiredResultType == InvalidOid);
327 : : }
328 : :
329 : : /*
330 : : * look up the aggregate's transtype.
331 : : *
332 : : * transtype can't be a pseudo-type, since we need to be able to store
333 : : * values of the transtype. However, we can allow polymorphic transtype
334 : : * in some cases (AggregateCreate will check). Also, we allow "internal"
335 : : * for functions that want to pass pointers to private data structures;
336 : : * but allow that only to superusers, since you could crash the system (or
337 : : * worse) by connecting up incompatible internal-using functions in an
338 : : * aggregate.
339 : : */
340 : 569 : transTypeId = typenameTypeId(NULL, transType);
341 : 569 : transTypeType = get_typtype(transTypeId);
342 [ + + + + ]: 569 : if (transTypeType == TYPTYPE_PSEUDO &&
343 [ + + + - : 184 : !IsPolymorphicType(transTypeId))
+ - + - +
- + + + -
+ - + - +
- ]
344 : : {
345 [ + - + - ]: 38 : if (transTypeId == INTERNALOID && superuser())
346 : : /* okay */ ;
347 : : else
348 [ # # ]: 0 : ereport(ERROR,
349 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
350 : : errmsg("aggregate transition data type cannot be %s",
351 : : format_type_be(transTypeId))));
352 : : }
353 : :
354 [ + + + + ]: 569 : if (serialfuncName && deserialfuncName)
355 : : {
356 : : /*
357 : : * Serialization is only needed/allowed for transtype INTERNAL.
358 : : */
359 [ - + ]: 20 : if (transTypeId != INTERNALOID)
360 [ # # ]: 0 : ereport(ERROR,
361 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
362 : : errmsg("serialization functions may be specified only when the aggregate transition data type is %s",
363 : : format_type_be(INTERNALOID))));
364 : : }
365 [ + + - + ]: 549 : else if (serialfuncName || deserialfuncName)
366 : : {
367 : : /*
368 : : * Cannot specify one function without the other.
369 : : */
370 [ + - ]: 4 : ereport(ERROR,
371 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
372 : : errmsg("must specify both or neither of serialization and deserialization functions")));
373 : : }
374 : :
375 : : /*
376 : : * If a moving-aggregate transtype is specified, look that up. Same
377 : : * restrictions as for transtype.
378 : : */
379 [ + + ]: 565 : if (mtransType)
380 : : {
381 : 38 : mtransTypeId = typenameTypeId(NULL, mtransType);
382 : 38 : mtransTypeType = get_typtype(mtransTypeId);
383 [ - + - - ]: 38 : if (mtransTypeType == TYPTYPE_PSEUDO &&
384 [ # # # # : 0 : !IsPolymorphicType(mtransTypeId))
# # # # #
# # # # #
# # # # #
# ]
385 : : {
386 [ # # # # ]: 0 : if (mtransTypeId == INTERNALOID && superuser())
387 : : /* okay */ ;
388 : : else
389 [ # # ]: 0 : ereport(ERROR,
390 : : (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
391 : : errmsg("aggregate transition data type cannot be %s",
392 : : format_type_be(mtransTypeId))));
393 : : }
394 : : }
395 : :
396 : : /*
397 : : * If we have an initval, and it's not for a pseudotype (particularly a
398 : : * polymorphic type), make sure it's acceptable to the type's input
399 : : * function. We will store the initval as text, because the input
400 : : * function isn't necessarily immutable (consider "now" for timestamp),
401 : : * and we want to use the runtime not creation-time interpretation of the
402 : : * value. However, if it's an incorrect value it seems much more
403 : : * user-friendly to complain at CREATE AGGREGATE time.
404 : : */
405 [ + + + + ]: 565 : if (initval && transTypeType != TYPTYPE_PSEUDO)
406 : : {
407 : : Oid typinput,
408 : : typioparam;
409 : :
410 : 239 : getTypeInputInfo(transTypeId, &typinput, &typioparam);
411 : 239 : (void) OidInputFunctionCall(typinput, initval, typioparam, -1);
412 : : }
413 : :
414 : : /*
415 : : * Likewise for moving-aggregate initval.
416 : : */
417 [ + + + - ]: 565 : if (minitval && mtransTypeType != TYPTYPE_PSEUDO)
418 : : {
419 : : Oid typinput,
420 : : typioparam;
421 : :
422 : 10 : getTypeInputInfo(mtransTypeId, &typinput, &typioparam);
423 : 10 : (void) OidInputFunctionCall(typinput, minitval, typioparam, -1);
424 : : }
425 : :
426 [ + + ]: 565 : if (parallel)
427 : : {
428 [ + + ]: 21 : if (strcmp(parallel, "safe") == 0)
429 : 17 : proparallel = PROPARALLEL_SAFE;
430 [ - + ]: 4 : else if (strcmp(parallel, "restricted") == 0)
431 : 0 : proparallel = PROPARALLEL_RESTRICTED;
432 [ - + ]: 4 : else if (strcmp(parallel, "unsafe") == 0)
433 : 0 : proparallel = PROPARALLEL_UNSAFE;
434 : : else
435 [ + - ]: 4 : ereport(ERROR,
436 : : (errcode(ERRCODE_SYNTAX_ERROR),
437 : : errmsg("parameter \"parallel\" must be SAFE, RESTRICTED, or UNSAFE")));
438 : : }
439 : :
440 : : /*
441 : : * Most of the argument-checking is done inside of AggregateCreate
442 : : */
443 : 561 : return AggregateCreate(aggName, /* aggregate name */
444 : : aggNamespace, /* namespace */
445 : : replace,
446 : : aggKind,
447 : : numArgs,
448 : : numDirectArgs,
449 : : parameterTypes,
450 : : PointerGetDatum(allParameterTypes),
451 : : PointerGetDatum(parameterModes),
452 : : PointerGetDatum(parameterNames),
453 : : parameterDefaults,
454 : : variadicArgType,
455 : : transfuncName, /* step function name */
456 : : finalfuncName, /* final function name */
457 : : combinefuncName, /* combine function name */
458 : : serialfuncName, /* serial function name */
459 : : deserialfuncName, /* deserial function name */
460 : : mtransfuncName, /* fwd trans function name */
461 : : minvtransfuncName, /* inv trans function name */
462 : : mfinalfuncName, /* final function name */
463 : : finalfuncExtraArgs,
464 : : mfinalfuncExtraArgs,
465 : : finalfuncModify,
466 : : mfinalfuncModify,
467 : : sortoperatorName, /* sort operator name */
468 : : supportfuncName, /* planner support func name */
469 : : transTypeId, /* transition data type */
470 : : transSpace, /* transition space */
471 : : mtransTypeId, /* transition data type */
472 : : mtransSpace, /* transition space */
473 : : initval, /* initial condition */
474 : : minitval, /* initial condition */
475 : : proparallel); /* parallel safe? */
476 : : }
477 : :
478 : : /*
479 : : * Convert the string form of [m]finalfunc_modify to the catalog representation
480 : : */
481 : : static char
482 : 13 : extractModify(DefElem *defel)
483 : : {
484 : 13 : char *val = defGetString(defel);
485 : :
486 [ - + ]: 13 : if (strcmp(val, "read_only") == 0)
487 : 0 : return AGGMODIFY_READ_ONLY;
488 [ + + ]: 13 : if (strcmp(val, "shareable") == 0)
489 : 9 : return AGGMODIFY_SHAREABLE;
490 [ + - ]: 4 : if (strcmp(val, "read_write") == 0)
491 : 4 : return AGGMODIFY_READ_WRITE;
492 [ # # ]: 0 : ereport(ERROR,
493 : : (errcode(ERRCODE_SYNTAX_ERROR),
494 : : errmsg("parameter \"%s\" must be READ_ONLY, SHAREABLE, or READ_WRITE",
495 : : defel->defname)));
496 : : return 0; /* keep compiler quiet */
497 : : }
|