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