/src/postgres/src/backend/access/rmgrdesc/heapdesc.c
Line | Count | Source |
1 | | /*------------------------------------------------------------------------- |
2 | | * |
3 | | * heapdesc.c |
4 | | * rmgr descriptor routines for access/heap/heapam.c |
5 | | * |
6 | | * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group |
7 | | * Portions Copyright (c) 1994, Regents of the University of California |
8 | | * |
9 | | * |
10 | | * IDENTIFICATION |
11 | | * src/backend/access/rmgrdesc/heapdesc.c |
12 | | * |
13 | | *------------------------------------------------------------------------- |
14 | | */ |
15 | | #include "postgres.h" |
16 | | |
17 | | #include "access/heapam_xlog.h" |
18 | | #include "access/rmgrdesc_utils.h" |
19 | | #include "access/visibilitymapdefs.h" |
20 | | #include "storage/standbydefs.h" |
21 | | |
22 | | /* |
23 | | * NOTE: "keyname" argument cannot have trailing spaces or punctuation |
24 | | * characters |
25 | | */ |
26 | | static void |
27 | | infobits_desc(StringInfo buf, uint8 infobits, const char *keyname) |
28 | 0 | { |
29 | 0 | appendStringInfo(buf, "%s: [", keyname); |
30 | |
|
31 | 0 | Assert(buf->data[buf->len - 1] != ' '); |
32 | |
|
33 | 0 | if (infobits & XLHL_XMAX_IS_MULTI) |
34 | 0 | appendStringInfoString(buf, "IS_MULTI, "); |
35 | 0 | if (infobits & XLHL_XMAX_LOCK_ONLY) |
36 | 0 | appendStringInfoString(buf, "LOCK_ONLY, "); |
37 | 0 | if (infobits & XLHL_XMAX_EXCL_LOCK) |
38 | 0 | appendStringInfoString(buf, "EXCL_LOCK, "); |
39 | 0 | if (infobits & XLHL_XMAX_KEYSHR_LOCK) |
40 | 0 | appendStringInfoString(buf, "KEYSHR_LOCK, "); |
41 | 0 | if (infobits & XLHL_KEYS_UPDATED) |
42 | 0 | appendStringInfoString(buf, "KEYS_UPDATED, "); |
43 | |
|
44 | 0 | if (buf->data[buf->len - 1] == ' ') |
45 | 0 | { |
46 | | /* Truncate-away final unneeded ", " */ |
47 | 0 | Assert(buf->data[buf->len - 2] == ','); |
48 | 0 | buf->len -= 2; |
49 | 0 | buf->data[buf->len] = '\0'; |
50 | 0 | } |
51 | |
|
52 | 0 | appendStringInfoChar(buf, ']'); |
53 | 0 | } |
54 | | |
55 | | static void |
56 | | truncate_flags_desc(StringInfo buf, uint8 flags) |
57 | 0 | { |
58 | 0 | appendStringInfoString(buf, "flags: ["); |
59 | |
|
60 | 0 | if (flags & XLH_TRUNCATE_CASCADE) |
61 | 0 | appendStringInfoString(buf, "CASCADE, "); |
62 | 0 | if (flags & XLH_TRUNCATE_RESTART_SEQS) |
63 | 0 | appendStringInfoString(buf, "RESTART_SEQS, "); |
64 | |
|
65 | 0 | if (buf->data[buf->len - 1] == ' ') |
66 | 0 | { |
67 | | /* Truncate-away final unneeded ", " */ |
68 | 0 | Assert(buf->data[buf->len - 2] == ','); |
69 | 0 | buf->len -= 2; |
70 | 0 | buf->data[buf->len] = '\0'; |
71 | 0 | } |
72 | |
|
73 | 0 | appendStringInfoChar(buf, ']'); |
74 | 0 | } |
75 | | |
76 | | static void |
77 | | plan_elem_desc(StringInfo buf, void *plan, void *data) |
78 | 0 | { |
79 | 0 | xlhp_freeze_plan *new_plan = (xlhp_freeze_plan *) plan; |
80 | 0 | OffsetNumber **offsets = data; |
81 | |
|
82 | 0 | appendStringInfo(buf, "{ xmax: %u, infomask: %u, infomask2: %u, ntuples: %u", |
83 | 0 | new_plan->xmax, |
84 | 0 | new_plan->t_infomask, new_plan->t_infomask2, |
85 | 0 | new_plan->ntuples); |
86 | |
|
87 | 0 | appendStringInfoString(buf, ", offsets:"); |
88 | 0 | array_desc(buf, *offsets, sizeof(OffsetNumber), new_plan->ntuples, |
89 | 0 | &offset_elem_desc, NULL); |
90 | |
|
91 | 0 | *offsets += new_plan->ntuples; |
92 | |
|
93 | 0 | appendStringInfoString(buf, " }"); |
94 | 0 | } |
95 | | |
96 | | |
97 | | /* |
98 | | * Given a MAXALIGNed buffer returned by XLogRecGetBlockData() and pointed to |
99 | | * by cursor and any xl_heap_prune flags, deserialize the arrays of |
100 | | * OffsetNumbers contained in an XLOG_HEAP2_PRUNE_* record. |
101 | | * |
102 | | * This is in heapdesc.c so it can be shared between heap2_redo and heap2_desc |
103 | | * code, the latter of which is used in frontend (pg_waldump) code. |
104 | | */ |
105 | | void |
106 | | heap_xlog_deserialize_prune_and_freeze(char *cursor, uint16 flags, |
107 | | int *nplans, xlhp_freeze_plan **plans, |
108 | | OffsetNumber **frz_offsets, |
109 | | int *nredirected, OffsetNumber **redirected, |
110 | | int *ndead, OffsetNumber **nowdead, |
111 | | int *nunused, OffsetNumber **nowunused) |
112 | 0 | { |
113 | 0 | if (flags & XLHP_HAS_FREEZE_PLANS) |
114 | 0 | { |
115 | 0 | xlhp_freeze_plans *freeze_plans = (xlhp_freeze_plans *) cursor; |
116 | |
|
117 | 0 | *nplans = freeze_plans->nplans; |
118 | 0 | Assert(*nplans > 0); |
119 | 0 | *plans = freeze_plans->plans; |
120 | |
|
121 | 0 | cursor += offsetof(xlhp_freeze_plans, plans); |
122 | 0 | cursor += sizeof(xlhp_freeze_plan) * *nplans; |
123 | 0 | } |
124 | 0 | else |
125 | 0 | { |
126 | 0 | *nplans = 0; |
127 | 0 | *plans = NULL; |
128 | 0 | } |
129 | |
|
130 | 0 | if (flags & XLHP_HAS_REDIRECTIONS) |
131 | 0 | { |
132 | 0 | xlhp_prune_items *subrecord = (xlhp_prune_items *) cursor; |
133 | |
|
134 | 0 | *nredirected = subrecord->ntargets; |
135 | 0 | Assert(*nredirected > 0); |
136 | 0 | *redirected = &subrecord->data[0]; |
137 | |
|
138 | 0 | cursor += offsetof(xlhp_prune_items, data); |
139 | 0 | cursor += sizeof(OffsetNumber[2]) * *nredirected; |
140 | 0 | } |
141 | 0 | else |
142 | 0 | { |
143 | 0 | *nredirected = 0; |
144 | 0 | *redirected = NULL; |
145 | 0 | } |
146 | |
|
147 | 0 | if (flags & XLHP_HAS_DEAD_ITEMS) |
148 | 0 | { |
149 | 0 | xlhp_prune_items *subrecord = (xlhp_prune_items *) cursor; |
150 | |
|
151 | 0 | *ndead = subrecord->ntargets; |
152 | 0 | Assert(*ndead > 0); |
153 | 0 | *nowdead = subrecord->data; |
154 | |
|
155 | 0 | cursor += offsetof(xlhp_prune_items, data); |
156 | 0 | cursor += sizeof(OffsetNumber) * *ndead; |
157 | 0 | } |
158 | 0 | else |
159 | 0 | { |
160 | 0 | *ndead = 0; |
161 | 0 | *nowdead = NULL; |
162 | 0 | } |
163 | |
|
164 | 0 | if (flags & XLHP_HAS_NOW_UNUSED_ITEMS) |
165 | 0 | { |
166 | 0 | xlhp_prune_items *subrecord = (xlhp_prune_items *) cursor; |
167 | |
|
168 | 0 | *nunused = subrecord->ntargets; |
169 | 0 | Assert(*nunused > 0); |
170 | 0 | *nowunused = subrecord->data; |
171 | |
|
172 | 0 | cursor += offsetof(xlhp_prune_items, data); |
173 | 0 | cursor += sizeof(OffsetNumber) * *nunused; |
174 | 0 | } |
175 | 0 | else |
176 | 0 | { |
177 | 0 | *nunused = 0; |
178 | 0 | *nowunused = NULL; |
179 | 0 | } |
180 | |
|
181 | 0 | *frz_offsets = (OffsetNumber *) cursor; |
182 | 0 | } |
183 | | |
184 | | void |
185 | | heap_desc(StringInfo buf, XLogReaderState *record) |
186 | 0 | { |
187 | 0 | char *rec = XLogRecGetData(record); |
188 | 0 | uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; |
189 | |
|
190 | 0 | info &= XLOG_HEAP_OPMASK; |
191 | 0 | if (info == XLOG_HEAP_INSERT) |
192 | 0 | { |
193 | 0 | xl_heap_insert *xlrec = (xl_heap_insert *) rec; |
194 | |
|
195 | 0 | appendStringInfo(buf, "off: %u, flags: 0x%02X", |
196 | 0 | xlrec->offnum, |
197 | 0 | xlrec->flags); |
198 | 0 | } |
199 | 0 | else if (info == XLOG_HEAP_DELETE) |
200 | 0 | { |
201 | 0 | xl_heap_delete *xlrec = (xl_heap_delete *) rec; |
202 | |
|
203 | 0 | appendStringInfo(buf, "xmax: %u, off: %u, ", |
204 | 0 | xlrec->xmax, xlrec->offnum); |
205 | 0 | infobits_desc(buf, xlrec->infobits_set, "infobits"); |
206 | 0 | appendStringInfo(buf, ", flags: 0x%02X", xlrec->flags); |
207 | 0 | } |
208 | 0 | else if (info == XLOG_HEAP_UPDATE) |
209 | 0 | { |
210 | 0 | xl_heap_update *xlrec = (xl_heap_update *) rec; |
211 | |
|
212 | 0 | appendStringInfo(buf, "old_xmax: %u, old_off: %u, ", |
213 | 0 | xlrec->old_xmax, xlrec->old_offnum); |
214 | 0 | infobits_desc(buf, xlrec->old_infobits_set, "old_infobits"); |
215 | 0 | appendStringInfo(buf, ", flags: 0x%02X, new_xmax: %u, new_off: %u", |
216 | 0 | xlrec->flags, xlrec->new_xmax, xlrec->new_offnum); |
217 | 0 | } |
218 | 0 | else if (info == XLOG_HEAP_HOT_UPDATE) |
219 | 0 | { |
220 | 0 | xl_heap_update *xlrec = (xl_heap_update *) rec; |
221 | |
|
222 | 0 | appendStringInfo(buf, "old_xmax: %u, old_off: %u, ", |
223 | 0 | xlrec->old_xmax, xlrec->old_offnum); |
224 | 0 | infobits_desc(buf, xlrec->old_infobits_set, "old_infobits"); |
225 | 0 | appendStringInfo(buf, ", flags: 0x%02X, new_xmax: %u, new_off: %u", |
226 | 0 | xlrec->flags, xlrec->new_xmax, xlrec->new_offnum); |
227 | 0 | } |
228 | 0 | else if (info == XLOG_HEAP_TRUNCATE) |
229 | 0 | { |
230 | 0 | xl_heap_truncate *xlrec = (xl_heap_truncate *) rec; |
231 | |
|
232 | 0 | truncate_flags_desc(buf, xlrec->flags); |
233 | 0 | appendStringInfo(buf, ", nrelids: %u", xlrec->nrelids); |
234 | 0 | appendStringInfoString(buf, ", relids:"); |
235 | 0 | array_desc(buf, xlrec->relids, sizeof(Oid), xlrec->nrelids, |
236 | 0 | &oid_elem_desc, NULL); |
237 | 0 | } |
238 | 0 | else if (info == XLOG_HEAP_CONFIRM) |
239 | 0 | { |
240 | 0 | xl_heap_confirm *xlrec = (xl_heap_confirm *) rec; |
241 | |
|
242 | 0 | appendStringInfo(buf, "off: %u", xlrec->offnum); |
243 | 0 | } |
244 | 0 | else if (info == XLOG_HEAP_LOCK) |
245 | 0 | { |
246 | 0 | xl_heap_lock *xlrec = (xl_heap_lock *) rec; |
247 | |
|
248 | 0 | appendStringInfo(buf, "xmax: %u, off: %u, ", |
249 | 0 | xlrec->xmax, xlrec->offnum); |
250 | 0 | infobits_desc(buf, xlrec->infobits_set, "infobits"); |
251 | 0 | appendStringInfo(buf, ", flags: 0x%02X", xlrec->flags); |
252 | 0 | } |
253 | 0 | else if (info == XLOG_HEAP_INPLACE) |
254 | 0 | { |
255 | 0 | xl_heap_inplace *xlrec = (xl_heap_inplace *) rec; |
256 | |
|
257 | 0 | appendStringInfo(buf, "off: %u", xlrec->offnum); |
258 | 0 | standby_desc_invalidations(buf, xlrec->nmsgs, xlrec->msgs, |
259 | 0 | xlrec->dbId, xlrec->tsId, |
260 | 0 | xlrec->relcacheInitFileInval); |
261 | 0 | } |
262 | 0 | } |
263 | | |
264 | | void |
265 | | heap2_desc(StringInfo buf, XLogReaderState *record) |
266 | 0 | { |
267 | 0 | char *rec = XLogRecGetData(record); |
268 | 0 | uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; |
269 | |
|
270 | 0 | info &= XLOG_HEAP_OPMASK; |
271 | 0 | if (info == XLOG_HEAP2_PRUNE_ON_ACCESS || |
272 | 0 | info == XLOG_HEAP2_PRUNE_VACUUM_SCAN || |
273 | 0 | info == XLOG_HEAP2_PRUNE_VACUUM_CLEANUP) |
274 | 0 | { |
275 | 0 | xl_heap_prune *xlrec = (xl_heap_prune *) rec; |
276 | |
|
277 | 0 | if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON) |
278 | 0 | { |
279 | 0 | TransactionId conflict_xid; |
280 | |
|
281 | 0 | memcpy(&conflict_xid, rec + SizeOfHeapPrune, sizeof(TransactionId)); |
282 | |
|
283 | 0 | appendStringInfo(buf, "snapshotConflictHorizon: %u", |
284 | 0 | conflict_xid); |
285 | 0 | } |
286 | |
|
287 | 0 | appendStringInfo(buf, ", isCatalogRel: %c", |
288 | 0 | xlrec->flags & XLHP_IS_CATALOG_REL ? 'T' : 'F'); |
289 | |
|
290 | 0 | if (xlrec->flags & XLHP_VM_ALL_VISIBLE) |
291 | 0 | { |
292 | 0 | uint8 vmflags = VISIBILITYMAP_ALL_VISIBLE; |
293 | |
|
294 | 0 | if (xlrec->flags & XLHP_VM_ALL_FROZEN) |
295 | 0 | vmflags |= VISIBILITYMAP_ALL_FROZEN; |
296 | 0 | appendStringInfo(buf, ", vm_flags: 0x%02X", vmflags); |
297 | 0 | } |
298 | |
|
299 | 0 | if (XLogRecHasBlockData(record, 0)) |
300 | 0 | { |
301 | 0 | Size datalen; |
302 | 0 | OffsetNumber *redirected; |
303 | 0 | OffsetNumber *nowdead; |
304 | 0 | OffsetNumber *nowunused; |
305 | 0 | int nredirected; |
306 | 0 | int nunused; |
307 | 0 | int ndead; |
308 | 0 | int nplans; |
309 | 0 | xlhp_freeze_plan *plans; |
310 | 0 | OffsetNumber *frz_offsets; |
311 | |
|
312 | 0 | char *cursor = XLogRecGetBlockData(record, 0, &datalen); |
313 | |
|
314 | 0 | heap_xlog_deserialize_prune_and_freeze(cursor, xlrec->flags, |
315 | 0 | &nplans, &plans, &frz_offsets, |
316 | 0 | &nredirected, &redirected, |
317 | 0 | &ndead, &nowdead, |
318 | 0 | &nunused, &nowunused); |
319 | |
|
320 | 0 | appendStringInfo(buf, ", nplans: %u, nredirected: %u, ndead: %u, nunused: %u", |
321 | 0 | nplans, nredirected, ndead, nunused); |
322 | |
|
323 | 0 | if (nplans > 0) |
324 | 0 | { |
325 | 0 | appendStringInfoString(buf, ", plans:"); |
326 | 0 | array_desc(buf, plans, sizeof(xlhp_freeze_plan), nplans, |
327 | 0 | &plan_elem_desc, &frz_offsets); |
328 | 0 | } |
329 | |
|
330 | 0 | if (nredirected > 0) |
331 | 0 | { |
332 | 0 | appendStringInfoString(buf, ", redirected:"); |
333 | 0 | array_desc(buf, redirected, sizeof(OffsetNumber) * 2, |
334 | 0 | nredirected, &redirect_elem_desc, NULL); |
335 | 0 | } |
336 | |
|
337 | 0 | if (ndead > 0) |
338 | 0 | { |
339 | 0 | appendStringInfoString(buf, ", dead:"); |
340 | 0 | array_desc(buf, nowdead, sizeof(OffsetNumber), ndead, |
341 | 0 | &offset_elem_desc, NULL); |
342 | 0 | } |
343 | |
|
344 | 0 | if (nunused > 0) |
345 | 0 | { |
346 | 0 | appendStringInfoString(buf, ", unused:"); |
347 | 0 | array_desc(buf, nowunused, sizeof(OffsetNumber), nunused, |
348 | 0 | &offset_elem_desc, NULL); |
349 | 0 | } |
350 | 0 | } |
351 | 0 | } |
352 | 0 | else if (info == XLOG_HEAP2_MULTI_INSERT) |
353 | 0 | { |
354 | 0 | xl_heap_multi_insert *xlrec = (xl_heap_multi_insert *) rec; |
355 | 0 | bool isinit = (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) != 0; |
356 | |
|
357 | 0 | appendStringInfo(buf, "ntuples: %d, flags: 0x%02X", xlrec->ntuples, |
358 | 0 | xlrec->flags); |
359 | |
|
360 | 0 | if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) |
361 | 0 | appendStringInfo(buf, ", vm_flags: 0x%02X", |
362 | 0 | VISIBILITYMAP_ALL_VISIBLE | |
363 | 0 | VISIBILITYMAP_ALL_FROZEN); |
364 | |
|
365 | 0 | if (XLogRecHasBlockData(record, 0) && !isinit) |
366 | 0 | { |
367 | 0 | appendStringInfoString(buf, ", offsets:"); |
368 | 0 | array_desc(buf, xlrec->offsets, sizeof(OffsetNumber), |
369 | 0 | xlrec->ntuples, &offset_elem_desc, NULL); |
370 | 0 | } |
371 | 0 | } |
372 | 0 | else if (info == XLOG_HEAP2_LOCK_UPDATED) |
373 | 0 | { |
374 | 0 | xl_heap_lock_updated *xlrec = (xl_heap_lock_updated *) rec; |
375 | |
|
376 | 0 | appendStringInfo(buf, "xmax: %u, off: %u, ", |
377 | 0 | xlrec->xmax, xlrec->offnum); |
378 | 0 | infobits_desc(buf, xlrec->infobits_set, "infobits"); |
379 | 0 | appendStringInfo(buf, ", flags: 0x%02X", xlrec->flags); |
380 | 0 | } |
381 | 0 | else if (info == XLOG_HEAP2_NEW_CID) |
382 | 0 | { |
383 | 0 | xl_heap_new_cid *xlrec = (xl_heap_new_cid *) rec; |
384 | |
|
385 | 0 | appendStringInfo(buf, "rel: %u/%u/%u, tid: %u/%u", |
386 | 0 | xlrec->target_locator.spcOid, |
387 | 0 | xlrec->target_locator.dbOid, |
388 | 0 | xlrec->target_locator.relNumber, |
389 | 0 | ItemPointerGetBlockNumber(&(xlrec->target_tid)), |
390 | 0 | ItemPointerGetOffsetNumber(&(xlrec->target_tid))); |
391 | 0 | appendStringInfo(buf, ", cmin: %u, cmax: %u, combo: %u", |
392 | 0 | xlrec->cmin, xlrec->cmax, xlrec->combocid); |
393 | 0 | } |
394 | 0 | } |
395 | | |
396 | | const char * |
397 | | heap_identify(uint8 info) |
398 | 0 | { |
399 | 0 | const char *id = NULL; |
400 | |
|
401 | 0 | switch (info & ~XLR_INFO_MASK) |
402 | 0 | { |
403 | 0 | case XLOG_HEAP_INSERT: |
404 | 0 | id = "INSERT"; |
405 | 0 | break; |
406 | 0 | case XLOG_HEAP_INSERT | XLOG_HEAP_INIT_PAGE: |
407 | 0 | id = "INSERT+INIT"; |
408 | 0 | break; |
409 | 0 | case XLOG_HEAP_DELETE: |
410 | 0 | id = "DELETE"; |
411 | 0 | break; |
412 | 0 | case XLOG_HEAP_UPDATE: |
413 | 0 | id = "UPDATE"; |
414 | 0 | break; |
415 | 0 | case XLOG_HEAP_UPDATE | XLOG_HEAP_INIT_PAGE: |
416 | 0 | id = "UPDATE+INIT"; |
417 | 0 | break; |
418 | 0 | case XLOG_HEAP_HOT_UPDATE: |
419 | 0 | id = "HOT_UPDATE"; |
420 | 0 | break; |
421 | 0 | case XLOG_HEAP_HOT_UPDATE | XLOG_HEAP_INIT_PAGE: |
422 | 0 | id = "HOT_UPDATE+INIT"; |
423 | 0 | break; |
424 | 0 | case XLOG_HEAP_TRUNCATE: |
425 | 0 | id = "TRUNCATE"; |
426 | 0 | break; |
427 | 0 | case XLOG_HEAP_CONFIRM: |
428 | 0 | id = "HEAP_CONFIRM"; |
429 | 0 | break; |
430 | 0 | case XLOG_HEAP_LOCK: |
431 | 0 | id = "LOCK"; |
432 | 0 | break; |
433 | 0 | case XLOG_HEAP_INPLACE: |
434 | 0 | id = "INPLACE"; |
435 | 0 | break; |
436 | 0 | } |
437 | | |
438 | 0 | return id; |
439 | 0 | } |
440 | | |
441 | | const char * |
442 | | heap2_identify(uint8 info) |
443 | 0 | { |
444 | 0 | const char *id = NULL; |
445 | |
|
446 | 0 | switch (info & ~XLR_INFO_MASK) |
447 | 0 | { |
448 | 0 | case XLOG_HEAP2_PRUNE_ON_ACCESS: |
449 | 0 | id = "PRUNE_ON_ACCESS"; |
450 | 0 | break; |
451 | 0 | case XLOG_HEAP2_PRUNE_VACUUM_SCAN: |
452 | 0 | id = "PRUNE_VACUUM_SCAN"; |
453 | 0 | break; |
454 | 0 | case XLOG_HEAP2_PRUNE_VACUUM_CLEANUP: |
455 | 0 | id = "PRUNE_VACUUM_CLEANUP"; |
456 | 0 | break; |
457 | 0 | case XLOG_HEAP2_MULTI_INSERT: |
458 | 0 | id = "MULTI_INSERT"; |
459 | 0 | break; |
460 | 0 | case XLOG_HEAP2_MULTI_INSERT | XLOG_HEAP_INIT_PAGE: |
461 | 0 | id = "MULTI_INSERT+INIT"; |
462 | 0 | break; |
463 | 0 | case XLOG_HEAP2_LOCK_UPDATED: |
464 | 0 | id = "LOCK_UPDATED"; |
465 | 0 | break; |
466 | 0 | case XLOG_HEAP2_NEW_CID: |
467 | 0 | id = "NEW_CID"; |
468 | 0 | break; |
469 | 0 | case XLOG_HEAP2_REWRITE: |
470 | 0 | id = "REWRITE"; |
471 | 0 | break; |
472 | 0 | } |
473 | | |
474 | 0 | return id; |
475 | 0 | } |