Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * enum.c
4 : * I/O functions, operators, aggregates etc for enum types
5 : *
6 : * Copyright (c) 2006-2025, PostgreSQL Global Development Group
7 : *
8 : *
9 : * IDENTIFICATION
10 : * src/backend/utils/adt/enum.c
11 : *
12 : *-------------------------------------------------------------------------
13 : */
14 : #include "postgres.h"
15 :
16 : #include "access/genam.h"
17 : #include "access/htup_details.h"
18 : #include "access/table.h"
19 : #include "catalog/pg_enum.h"
20 : #include "libpq/pqformat.h"
21 : #include "storage/procarray.h"
22 : #include "utils/array.h"
23 : #include "utils/builtins.h"
24 : #include "utils/fmgroids.h"
25 : #include "utils/syscache.h"
26 : #include "utils/typcache.h"
27 :
28 :
29 : static Oid enum_endpoint(Oid enumtypoid, ScanDirection direction);
30 : static ArrayType *enum_range_internal(Oid enumtypoid, Oid lower, Oid upper);
31 :
32 :
33 : /*
34 : * Disallow use of an uncommitted pg_enum tuple.
35 : *
36 : * We need to make sure that uncommitted enum values don't get into indexes.
37 : * If they did, and if we then rolled back the pg_enum addition, we'd have
38 : * broken the index because value comparisons will not work reliably without
39 : * an underlying pg_enum entry. (Note that removal of the heap entry
40 : * containing an enum value is not sufficient to ensure that it doesn't appear
41 : * in upper levels of indexes.) To do this we prevent an uncommitted row from
42 : * being used for any SQL-level purpose. This is stronger than necessary,
43 : * since the value might not be getting inserted into a table or there might
44 : * be no index on its column, but it's easy to enforce centrally.
45 : *
46 : * However, it's okay to allow use of uncommitted values belonging to enum
47 : * types that were themselves created in the same transaction, because then
48 : * any such index would also be new and would go away altogether on rollback.
49 : * We don't implement that fully right now, but we do allow free use of enum
50 : * values created during CREATE TYPE AS ENUM, which are surely of the same
51 : * lifespan as the enum type. (This case is required by "pg_restore -1".)
52 : * Values added by ALTER TYPE ADD VALUE are also allowed if the enum type
53 : * is known to have been created earlier in the same transaction. (Note that
54 : * we have to track that explicitly; comparing tuple xmins is insufficient,
55 : * because the type tuple might have been updated in the current transaction.
56 : * Subtransactions also create hazards to be accounted for; currently,
57 : * pg_enum.c only handles ADD VALUE at the outermost transaction level.)
58 : *
59 : * This function needs to be called (directly or indirectly) in any of the
60 : * functions below that could return an enum value to SQL operations.
61 : */
62 : static void
63 234854 : check_safe_enum_use(HeapTuple enumval_tup)
64 : {
65 : TransactionId xmin;
66 234854 : Form_pg_enum en = (Form_pg_enum) GETSTRUCT(enumval_tup);
67 :
68 : /*
69 : * If the row is hinted as committed, it's surely safe. This provides a
70 : * fast path for all normal use-cases.
71 : */
72 234854 : if (HeapTupleHeaderXminCommitted(enumval_tup->t_data))
73 234744 : return;
74 :
75 : /*
76 : * Usually, a row would get hinted as committed when it's read or loaded
77 : * into syscache; but just in case not, let's check the xmin directly.
78 : */
79 110 : xmin = HeapTupleHeaderGetXmin(enumval_tup->t_data);
80 130 : if (!TransactionIdIsInProgress(xmin) &&
81 20 : TransactionIdDidCommit(xmin))
82 20 : return;
83 :
84 : /*
85 : * Check if the enum value is listed as uncommitted. If not, it's safe,
86 : * because it can't be shorter-lived than its owning type. (This'd also
87 : * be false for values made by other transactions; but the previous tests
88 : * should have handled all of those.)
89 : */
90 90 : if (!EnumUncommitted(en->oid))
91 66 : return;
92 :
93 : /*
94 : * There might well be other tests we could do here to narrow down the
95 : * unsafe conditions, but for now just raise an exception.
96 : */
97 24 : ereport(ERROR,
98 : (errcode(ERRCODE_UNSAFE_NEW_ENUM_VALUE_USAGE),
99 : errmsg("unsafe use of new value \"%s\" of enum type %s",
100 : NameStr(en->enumlabel),
101 : format_type_be(en->enumtypid)),
102 : errhint("New enum values must be committed before they can be used.")));
103 : }
104 :
105 :
106 : /* Basic I/O support */
107 :
108 : Datum
109 234632 : enum_in(PG_FUNCTION_ARGS)
110 : {
111 234632 : char *name = PG_GETARG_CSTRING(0);
112 234632 : Oid enumtypoid = PG_GETARG_OID(1);
113 234632 : Node *escontext = fcinfo->context;
114 : Oid enumoid;
115 : HeapTuple tup;
116 :
117 : /* must check length to prevent Assert failure within SearchSysCache */
118 234632 : if (strlen(name) >= NAMEDATALEN)
119 6 : ereturn(escontext, (Datum) 0,
120 : (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
121 : errmsg("invalid input value for enum %s: \"%s\"",
122 : format_type_be(enumtypoid),
123 : name)));
124 :
125 234626 : tup = SearchSysCache2(ENUMTYPOIDNAME,
126 : ObjectIdGetDatum(enumtypoid),
127 : CStringGetDatum(name));
128 234626 : if (!HeapTupleIsValid(tup))
129 18 : ereturn(escontext, (Datum) 0,
130 : (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
131 : errmsg("invalid input value for enum %s: \"%s\"",
132 : format_type_be(enumtypoid),
133 : name)));
134 :
135 : /*
136 : * Check it's safe to use in SQL. Perhaps we should take the trouble to
137 : * report "unsafe use" softly; but it's unclear that it's worth the
138 : * trouble, or indeed that that is a legitimate bad-input case at all
139 : * rather than an implementation shortcoming.
140 : */
141 234608 : check_safe_enum_use(tup);
142 :
143 : /*
144 : * This comes from pg_enum.oid and stores system oids in user tables. This
145 : * oid must be preserved by binary upgrades.
146 : */
147 234596 : enumoid = ((Form_pg_enum) GETSTRUCT(tup))->oid;
148 :
149 234596 : ReleaseSysCache(tup);
150 :
151 234596 : PG_RETURN_OID(enumoid);
152 : }
153 :
154 : Datum
155 49122 : enum_out(PG_FUNCTION_ARGS)
156 : {
157 49122 : Oid enumval = PG_GETARG_OID(0);
158 : char *result;
159 : HeapTuple tup;
160 : Form_pg_enum en;
161 :
162 49122 : tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(enumval));
163 49122 : if (!HeapTupleIsValid(tup))
164 0 : ereport(ERROR,
165 : (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
166 : errmsg("invalid internal value for enum: %u",
167 : enumval)));
168 49122 : en = (Form_pg_enum) GETSTRUCT(tup);
169 :
170 49122 : result = pstrdup(NameStr(en->enumlabel));
171 :
172 49122 : ReleaseSysCache(tup);
173 :
174 49122 : PG_RETURN_CSTRING(result);
175 : }
176 :
177 : /* Binary I/O support */
178 : Datum
179 0 : enum_recv(PG_FUNCTION_ARGS)
180 : {
181 0 : StringInfo buf = (StringInfo) PG_GETARG_POINTER(0);
182 0 : Oid enumtypoid = PG_GETARG_OID(1);
183 : Oid enumoid;
184 : HeapTuple tup;
185 : char *name;
186 : int nbytes;
187 :
188 0 : name = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes);
189 :
190 : /* must check length to prevent Assert failure within SearchSysCache */
191 0 : if (strlen(name) >= NAMEDATALEN)
192 0 : ereport(ERROR,
193 : (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
194 : errmsg("invalid input value for enum %s: \"%s\"",
195 : format_type_be(enumtypoid),
196 : name)));
197 :
198 0 : tup = SearchSysCache2(ENUMTYPOIDNAME,
199 : ObjectIdGetDatum(enumtypoid),
200 : CStringGetDatum(name));
201 0 : if (!HeapTupleIsValid(tup))
202 0 : ereport(ERROR,
203 : (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
204 : errmsg("invalid input value for enum %s: \"%s\"",
205 : format_type_be(enumtypoid),
206 : name)));
207 :
208 : /* check it's safe to use in SQL */
209 0 : check_safe_enum_use(tup);
210 :
211 0 : enumoid = ((Form_pg_enum) GETSTRUCT(tup))->oid;
212 :
213 0 : ReleaseSysCache(tup);
214 :
215 0 : pfree(name);
216 :
217 0 : PG_RETURN_OID(enumoid);
218 : }
219 :
220 : Datum
221 0 : enum_send(PG_FUNCTION_ARGS)
222 : {
223 0 : Oid enumval = PG_GETARG_OID(0);
224 : StringInfoData buf;
225 : HeapTuple tup;
226 : Form_pg_enum en;
227 :
228 0 : tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(enumval));
229 0 : if (!HeapTupleIsValid(tup))
230 0 : ereport(ERROR,
231 : (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
232 : errmsg("invalid internal value for enum: %u",
233 : enumval)));
234 0 : en = (Form_pg_enum) GETSTRUCT(tup);
235 :
236 0 : pq_begintypsend(&buf);
237 0 : pq_sendtext(&buf, NameStr(en->enumlabel), strlen(NameStr(en->enumlabel)));
238 :
239 0 : ReleaseSysCache(tup);
240 :
241 0 : PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
242 : }
243 :
244 : /* Comparison functions and related */
245 :
246 : /*
247 : * enum_cmp_internal is the common engine for all the visible comparison
248 : * functions, except for enum_eq and enum_ne which can just check for OID
249 : * equality directly.
250 : */
251 : static int
252 2473362 : enum_cmp_internal(Oid arg1, Oid arg2, FunctionCallInfo fcinfo)
253 : {
254 : TypeCacheEntry *tcache;
255 :
256 : /*
257 : * We don't need the typcache except in the hopefully-uncommon case that
258 : * one or both Oids are odd. This means that cursory testing of code that
259 : * fails to pass flinfo to an enum comparison function might not disclose
260 : * the oversight. To make such errors more obvious, Assert that we have a
261 : * place to cache even when we take a fast-path exit.
262 : */
263 : Assert(fcinfo->flinfo != NULL);
264 :
265 : /* Equal OIDs are equal no matter what */
266 2473362 : if (arg1 == arg2)
267 2247232 : return 0;
268 :
269 : /* Fast path: even-numbered Oids are known to compare correctly */
270 226130 : if ((arg1 & 1) == 0 && (arg2 & 1) == 0)
271 : {
272 76056 : if (arg1 < arg2)
273 10714 : return -1;
274 : else
275 65342 : return 1;
276 : }
277 :
278 : /* Locate the typcache entry for the enum type */
279 150074 : tcache = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
280 150074 : if (tcache == NULL)
281 : {
282 : HeapTuple enum_tup;
283 : Form_pg_enum en;
284 : Oid typeoid;
285 :
286 : /* Get the OID of the enum type containing arg1 */
287 8 : enum_tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(arg1));
288 8 : if (!HeapTupleIsValid(enum_tup))
289 0 : ereport(ERROR,
290 : (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
291 : errmsg("invalid internal value for enum: %u",
292 : arg1)));
293 8 : en = (Form_pg_enum) GETSTRUCT(enum_tup);
294 8 : typeoid = en->enumtypid;
295 8 : ReleaseSysCache(enum_tup);
296 : /* Now locate and remember the typcache entry */
297 8 : tcache = lookup_type_cache(typeoid, 0);
298 8 : fcinfo->flinfo->fn_extra = tcache;
299 : }
300 :
301 : /* The remaining comparison logic is in typcache.c */
302 150074 : return compare_values_of_enum(tcache, arg1, arg2);
303 : }
304 :
305 : Datum
306 4022 : enum_lt(PG_FUNCTION_ARGS)
307 : {
308 4022 : Oid a = PG_GETARG_OID(0);
309 4022 : Oid b = PG_GETARG_OID(1);
310 :
311 4022 : PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) < 0);
312 : }
313 :
314 : Datum
315 2210 : enum_le(PG_FUNCTION_ARGS)
316 : {
317 2210 : Oid a = PG_GETARG_OID(0);
318 2210 : Oid b = PG_GETARG_OID(1);
319 :
320 2210 : PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) <= 0);
321 : }
322 :
323 : Datum
324 8670 : enum_eq(PG_FUNCTION_ARGS)
325 : {
326 8670 : Oid a = PG_GETARG_OID(0);
327 8670 : Oid b = PG_GETARG_OID(1);
328 :
329 8670 : PG_RETURN_BOOL(a == b);
330 : }
331 :
332 : Datum
333 72 : enum_ne(PG_FUNCTION_ARGS)
334 : {
335 72 : Oid a = PG_GETARG_OID(0);
336 72 : Oid b = PG_GETARG_OID(1);
337 :
338 72 : PG_RETURN_BOOL(a != b);
339 : }
340 :
341 : Datum
342 2178 : enum_ge(PG_FUNCTION_ARGS)
343 : {
344 2178 : Oid a = PG_GETARG_OID(0);
345 2178 : Oid b = PG_GETARG_OID(1);
346 :
347 2178 : PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) >= 0);
348 : }
349 :
350 : Datum
351 3964 : enum_gt(PG_FUNCTION_ARGS)
352 : {
353 3964 : Oid a = PG_GETARG_OID(0);
354 3964 : Oid b = PG_GETARG_OID(1);
355 :
356 3964 : PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) > 0);
357 : }
358 :
359 : Datum
360 30 : enum_smaller(PG_FUNCTION_ARGS)
361 : {
362 30 : Oid a = PG_GETARG_OID(0);
363 30 : Oid b = PG_GETARG_OID(1);
364 :
365 30 : PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) < 0 ? a : b);
366 : }
367 :
368 : Datum
369 96 : enum_larger(PG_FUNCTION_ARGS)
370 : {
371 96 : Oid a = PG_GETARG_OID(0);
372 96 : Oid b = PG_GETARG_OID(1);
373 :
374 96 : PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) > 0 ? a : b);
375 : }
376 :
377 : Datum
378 2460862 : enum_cmp(PG_FUNCTION_ARGS)
379 : {
380 2460862 : Oid a = PG_GETARG_OID(0);
381 2460862 : Oid b = PG_GETARG_OID(1);
382 :
383 2460862 : PG_RETURN_INT32(enum_cmp_internal(a, b, fcinfo));
384 : }
385 :
386 : /* Enum programming support functions */
387 :
388 : /*
389 : * enum_endpoint: common code for enum_first/enum_last
390 : */
391 : static Oid
392 36 : enum_endpoint(Oid enumtypoid, ScanDirection direction)
393 : {
394 : Relation enum_rel;
395 : Relation enum_idx;
396 : SysScanDesc enum_scan;
397 : HeapTuple enum_tuple;
398 : ScanKeyData skey;
399 : Oid minmax;
400 :
401 : /*
402 : * Find the first/last enum member using pg_enum_typid_sortorder_index.
403 : * Note we must not use the syscache. See comments for RenumberEnumType
404 : * in catalog/pg_enum.c for more info.
405 : */
406 36 : ScanKeyInit(&skey,
407 : Anum_pg_enum_enumtypid,
408 : BTEqualStrategyNumber, F_OIDEQ,
409 : ObjectIdGetDatum(enumtypoid));
410 :
411 36 : enum_rel = table_open(EnumRelationId, AccessShareLock);
412 36 : enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
413 36 : enum_scan = systable_beginscan_ordered(enum_rel, enum_idx, NULL,
414 : 1, &skey);
415 :
416 36 : enum_tuple = systable_getnext_ordered(enum_scan, direction);
417 36 : if (HeapTupleIsValid(enum_tuple))
418 : {
419 : /* check it's safe to use in SQL */
420 36 : check_safe_enum_use(enum_tuple);
421 30 : minmax = ((Form_pg_enum) GETSTRUCT(enum_tuple))->oid;
422 : }
423 : else
424 : {
425 : /* should only happen with an empty enum */
426 0 : minmax = InvalidOid;
427 : }
428 :
429 30 : systable_endscan_ordered(enum_scan);
430 30 : index_close(enum_idx, AccessShareLock);
431 30 : table_close(enum_rel, AccessShareLock);
432 :
433 30 : return minmax;
434 : }
435 :
436 : Datum
437 12 : enum_first(PG_FUNCTION_ARGS)
438 : {
439 : Oid enumtypoid;
440 : Oid min;
441 :
442 : /*
443 : * We rely on being able to get the specific enum type from the calling
444 : * expression tree. Notice that the actual value of the argument isn't
445 : * examined at all; in particular it might be NULL.
446 : */
447 12 : enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
448 12 : if (enumtypoid == InvalidOid)
449 0 : ereport(ERROR,
450 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
451 : errmsg("could not determine actual enum type")));
452 :
453 : /* Get the OID using the index */
454 12 : min = enum_endpoint(enumtypoid, ForwardScanDirection);
455 :
456 12 : if (!OidIsValid(min))
457 0 : ereport(ERROR,
458 : (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
459 : errmsg("enum %s contains no values",
460 : format_type_be(enumtypoid))));
461 :
462 12 : PG_RETURN_OID(min);
463 : }
464 :
465 : Datum
466 24 : enum_last(PG_FUNCTION_ARGS)
467 : {
468 : Oid enumtypoid;
469 : Oid max;
470 :
471 : /*
472 : * We rely on being able to get the specific enum type from the calling
473 : * expression tree. Notice that the actual value of the argument isn't
474 : * examined at all; in particular it might be NULL.
475 : */
476 24 : enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
477 24 : if (enumtypoid == InvalidOid)
478 0 : ereport(ERROR,
479 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
480 : errmsg("could not determine actual enum type")));
481 :
482 : /* Get the OID using the index */
483 24 : max = enum_endpoint(enumtypoid, BackwardScanDirection);
484 :
485 18 : if (!OidIsValid(max))
486 0 : ereport(ERROR,
487 : (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
488 : errmsg("enum %s contains no values",
489 : format_type_be(enumtypoid))));
490 :
491 18 : PG_RETURN_OID(max);
492 : }
493 :
494 : /* 2-argument variant of enum_range */
495 : Datum
496 24 : enum_range_bounds(PG_FUNCTION_ARGS)
497 : {
498 : Oid lower;
499 : Oid upper;
500 : Oid enumtypoid;
501 :
502 24 : if (PG_ARGISNULL(0))
503 12 : lower = InvalidOid;
504 : else
505 12 : lower = PG_GETARG_OID(0);
506 24 : if (PG_ARGISNULL(1))
507 12 : upper = InvalidOid;
508 : else
509 12 : upper = PG_GETARG_OID(1);
510 :
511 : /*
512 : * We rely on being able to get the specific enum type from the calling
513 : * expression tree. The generic type mechanism should have ensured that
514 : * both are of the same type.
515 : */
516 24 : enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
517 24 : if (enumtypoid == InvalidOid)
518 0 : ereport(ERROR,
519 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
520 : errmsg("could not determine actual enum type")));
521 :
522 24 : PG_RETURN_ARRAYTYPE_P(enum_range_internal(enumtypoid, lower, upper));
523 : }
524 :
525 : /* 1-argument variant of enum_range */
526 : Datum
527 30 : enum_range_all(PG_FUNCTION_ARGS)
528 : {
529 : Oid enumtypoid;
530 :
531 : /*
532 : * We rely on being able to get the specific enum type from the calling
533 : * expression tree. Notice that the actual value of the argument isn't
534 : * examined at all; in particular it might be NULL.
535 : */
536 30 : enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
537 30 : if (enumtypoid == InvalidOid)
538 0 : ereport(ERROR,
539 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
540 : errmsg("could not determine actual enum type")));
541 :
542 30 : PG_RETURN_ARRAYTYPE_P(enum_range_internal(enumtypoid,
543 : InvalidOid, InvalidOid));
544 : }
545 :
546 : static ArrayType *
547 54 : enum_range_internal(Oid enumtypoid, Oid lower, Oid upper)
548 : {
549 : ArrayType *result;
550 : Relation enum_rel;
551 : Relation enum_idx;
552 : SysScanDesc enum_scan;
553 : HeapTuple enum_tuple;
554 : ScanKeyData skey;
555 : Datum *elems;
556 : int max,
557 : cnt;
558 : bool left_found;
559 :
560 : /*
561 : * Scan the enum members in order using pg_enum_typid_sortorder_index.
562 : * Note we must not use the syscache. See comments for RenumberEnumType
563 : * in catalog/pg_enum.c for more info.
564 : */
565 54 : ScanKeyInit(&skey,
566 : Anum_pg_enum_enumtypid,
567 : BTEqualStrategyNumber, F_OIDEQ,
568 : ObjectIdGetDatum(enumtypoid));
569 :
570 54 : enum_rel = table_open(EnumRelationId, AccessShareLock);
571 54 : enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
572 54 : enum_scan = systable_beginscan_ordered(enum_rel, enum_idx, NULL, 1, &skey);
573 :
574 54 : max = 64;
575 54 : elems = (Datum *) palloc(max * sizeof(Datum));
576 54 : cnt = 0;
577 54 : left_found = !OidIsValid(lower);
578 :
579 258 : while (HeapTupleIsValid(enum_tuple = systable_getnext_ordered(enum_scan, ForwardScanDirection)))
580 : {
581 222 : Oid enum_oid = ((Form_pg_enum) GETSTRUCT(enum_tuple))->oid;
582 :
583 222 : if (!left_found && lower == enum_oid)
584 12 : left_found = true;
585 :
586 222 : if (left_found)
587 : {
588 : /* check it's safe to use in SQL */
589 210 : check_safe_enum_use(enum_tuple);
590 :
591 204 : if (cnt >= max)
592 : {
593 0 : max *= 2;
594 0 : elems = (Datum *) repalloc(elems, max * sizeof(Datum));
595 : }
596 :
597 204 : elems[cnt++] = ObjectIdGetDatum(enum_oid);
598 : }
599 :
600 216 : if (OidIsValid(upper) && upper == enum_oid)
601 12 : break;
602 : }
603 :
604 48 : systable_endscan_ordered(enum_scan);
605 48 : index_close(enum_idx, AccessShareLock);
606 48 : table_close(enum_rel, AccessShareLock);
607 :
608 : /* and build the result array */
609 : /* note this hardwires some details about the representation of Oid */
610 48 : result = construct_array(elems, cnt, enumtypoid,
611 : sizeof(Oid), true, TYPALIGN_INT);
612 :
613 48 : pfree(elems);
614 :
615 48 : return result;
616 : }
|