/src/postgres/src/backend/access/spgist/spgxlog.c
Line | Count | Source |
1 | | /*------------------------------------------------------------------------- |
2 | | * |
3 | | * spgxlog.c |
4 | | * WAL replay logic for SP-GiST |
5 | | * |
6 | | * |
7 | | * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group |
8 | | * Portions Copyright (c) 1994, Regents of the University of California |
9 | | * |
10 | | * IDENTIFICATION |
11 | | * src/backend/access/spgist/spgxlog.c |
12 | | * |
13 | | *------------------------------------------------------------------------- |
14 | | */ |
15 | | #include "postgres.h" |
16 | | |
17 | | #include "access/bufmask.h" |
18 | | #include "access/spgist_private.h" |
19 | | #include "access/spgxlog.h" |
20 | | #include "access/xlogutils.h" |
21 | | #include "storage/standby.h" |
22 | | #include "utils/memutils.h" |
23 | | |
24 | | |
25 | | static MemoryContext opCtx; /* working memory for operations */ |
26 | | |
27 | | |
28 | | /* |
29 | | * Prepare a dummy SpGistState, with just the minimum info needed for replay. |
30 | | * |
31 | | * At present, all we need is enough info to support spgFormDeadTuple(), |
32 | | * plus the isBuild flag. |
33 | | */ |
34 | | static void |
35 | | fillFakeState(SpGistState *state, spgxlogState stateSrc) |
36 | 0 | { |
37 | 0 | memset(state, 0, sizeof(*state)); |
38 | |
|
39 | 0 | state->redirectXid = stateSrc.redirectXid; |
40 | 0 | state->isBuild = stateSrc.isBuild; |
41 | 0 | state->deadTupleStorage = palloc0(SGDTSIZE); |
42 | 0 | } |
43 | | |
44 | | /* |
45 | | * Add a leaf tuple, or replace an existing placeholder tuple. This is used |
46 | | * to replay SpGistPageAddNewItem() operations. If the offset points at an |
47 | | * existing tuple, it had better be a placeholder tuple. |
48 | | */ |
49 | | static void |
50 | | addOrReplaceTuple(Page page, const void *tuple, int size, OffsetNumber offset) |
51 | 0 | { |
52 | 0 | if (offset <= PageGetMaxOffsetNumber(page)) |
53 | 0 | { |
54 | 0 | SpGistDeadTuple dt = (SpGistDeadTuple) PageGetItem(page, |
55 | 0 | PageGetItemId(page, offset)); |
56 | |
|
57 | 0 | if (dt->tupstate != SPGIST_PLACEHOLDER) |
58 | 0 | elog(ERROR, "SPGiST tuple to be replaced is not a placeholder"); |
59 | | |
60 | 0 | Assert(SpGistPageGetOpaque(page)->nPlaceholder > 0); |
61 | 0 | SpGistPageGetOpaque(page)->nPlaceholder--; |
62 | |
|
63 | 0 | PageIndexTupleDelete(page, offset); |
64 | 0 | } |
65 | | |
66 | 0 | Assert(offset <= PageGetMaxOffsetNumber(page) + 1); |
67 | |
|
68 | 0 | if (PageAddItem(page, tuple, size, offset, false, false) != offset) |
69 | 0 | elog(ERROR, "failed to add item of size %u to SPGiST index page", |
70 | 0 | size); |
71 | 0 | } |
72 | | |
73 | | static void |
74 | | spgRedoAddLeaf(XLogReaderState *record) |
75 | 0 | { |
76 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
77 | 0 | char *ptr = XLogRecGetData(record); |
78 | 0 | spgxlogAddLeaf *xldata = (spgxlogAddLeaf *) ptr; |
79 | 0 | char *leafTuple; |
80 | 0 | SpGistLeafTupleData leafTupleHdr; |
81 | 0 | Buffer buffer; |
82 | 0 | Page page; |
83 | 0 | XLogRedoAction action; |
84 | |
|
85 | 0 | ptr += sizeof(spgxlogAddLeaf); |
86 | 0 | leafTuple = ptr; |
87 | | /* the leaf tuple is unaligned, so make a copy to access its header */ |
88 | 0 | memcpy(&leafTupleHdr, leafTuple, sizeof(SpGistLeafTupleData)); |
89 | | |
90 | | /* |
91 | | * In normal operation we would have both current and parent pages locked |
92 | | * simultaneously; but in WAL replay it should be safe to update the leaf |
93 | | * page before updating the parent. |
94 | | */ |
95 | 0 | if (xldata->newPage) |
96 | 0 | { |
97 | 0 | buffer = XLogInitBufferForRedo(record, 0); |
98 | 0 | SpGistInitBuffer(buffer, |
99 | 0 | SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); |
100 | 0 | action = BLK_NEEDS_REDO; |
101 | 0 | } |
102 | 0 | else |
103 | 0 | action = XLogReadBufferForRedo(record, 0, &buffer); |
104 | |
|
105 | 0 | if (action == BLK_NEEDS_REDO) |
106 | 0 | { |
107 | 0 | page = BufferGetPage(buffer); |
108 | | |
109 | | /* insert new tuple */ |
110 | 0 | if (xldata->offnumLeaf != xldata->offnumHeadLeaf) |
111 | 0 | { |
112 | | /* normal cases, tuple was added by SpGistPageAddNewItem */ |
113 | 0 | addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, xldata->offnumLeaf); |
114 | | |
115 | | /* update head tuple's chain link if needed */ |
116 | 0 | if (xldata->offnumHeadLeaf != InvalidOffsetNumber) |
117 | 0 | { |
118 | 0 | SpGistLeafTuple head; |
119 | |
|
120 | 0 | head = (SpGistLeafTuple) PageGetItem(page, |
121 | 0 | PageGetItemId(page, xldata->offnumHeadLeaf)); |
122 | 0 | Assert(SGLT_GET_NEXTOFFSET(head) == SGLT_GET_NEXTOFFSET(&leafTupleHdr)); |
123 | 0 | SGLT_SET_NEXTOFFSET(head, xldata->offnumLeaf); |
124 | 0 | } |
125 | 0 | } |
126 | 0 | else |
127 | 0 | { |
128 | | /* replacing a DEAD tuple */ |
129 | 0 | PageIndexTupleDelete(page, xldata->offnumLeaf); |
130 | 0 | if (PageAddItem(page, |
131 | 0 | leafTuple, leafTupleHdr.size, |
132 | 0 | xldata->offnumLeaf, false, false) != xldata->offnumLeaf) |
133 | 0 | elog(ERROR, "failed to add item of size %u to SPGiST index page", |
134 | 0 | leafTupleHdr.size); |
135 | 0 | } |
136 | | |
137 | 0 | PageSetLSN(page, lsn); |
138 | 0 | MarkBufferDirty(buffer); |
139 | 0 | } |
140 | 0 | if (BufferIsValid(buffer)) |
141 | 0 | UnlockReleaseBuffer(buffer); |
142 | | |
143 | | /* update parent downlink if necessary */ |
144 | 0 | if (xldata->offnumParent != InvalidOffsetNumber) |
145 | 0 | { |
146 | 0 | if (XLogReadBufferForRedo(record, 1, &buffer) == BLK_NEEDS_REDO) |
147 | 0 | { |
148 | 0 | SpGistInnerTuple tuple; |
149 | 0 | BlockNumber blknoLeaf; |
150 | |
|
151 | 0 | XLogRecGetBlockTag(record, 0, NULL, NULL, &blknoLeaf); |
152 | |
|
153 | 0 | page = BufferGetPage(buffer); |
154 | |
|
155 | 0 | tuple = (SpGistInnerTuple) PageGetItem(page, |
156 | 0 | PageGetItemId(page, xldata->offnumParent)); |
157 | |
|
158 | 0 | spgUpdateNodeLink(tuple, xldata->nodeI, |
159 | 0 | blknoLeaf, xldata->offnumLeaf); |
160 | |
|
161 | 0 | PageSetLSN(page, lsn); |
162 | 0 | MarkBufferDirty(buffer); |
163 | 0 | } |
164 | 0 | if (BufferIsValid(buffer)) |
165 | 0 | UnlockReleaseBuffer(buffer); |
166 | 0 | } |
167 | 0 | } |
168 | | |
169 | | static void |
170 | | spgRedoMoveLeafs(XLogReaderState *record) |
171 | 0 | { |
172 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
173 | 0 | char *ptr = XLogRecGetData(record); |
174 | 0 | spgxlogMoveLeafs *xldata = (spgxlogMoveLeafs *) ptr; |
175 | 0 | SpGistState state; |
176 | 0 | OffsetNumber *toDelete; |
177 | 0 | OffsetNumber *toInsert; |
178 | 0 | int nInsert; |
179 | 0 | Buffer buffer; |
180 | 0 | Page page; |
181 | 0 | XLogRedoAction action; |
182 | 0 | BlockNumber blknoDst; |
183 | |
|
184 | 0 | XLogRecGetBlockTag(record, 1, NULL, NULL, &blknoDst); |
185 | |
|
186 | 0 | fillFakeState(&state, xldata->stateSrc); |
187 | |
|
188 | 0 | nInsert = xldata->replaceDead ? 1 : xldata->nMoves + 1; |
189 | |
|
190 | 0 | ptr += SizeOfSpgxlogMoveLeafs; |
191 | 0 | toDelete = (OffsetNumber *) ptr; |
192 | 0 | ptr += sizeof(OffsetNumber) * xldata->nMoves; |
193 | 0 | toInsert = (OffsetNumber *) ptr; |
194 | 0 | ptr += sizeof(OffsetNumber) * nInsert; |
195 | | |
196 | | /* now ptr points to the list of leaf tuples */ |
197 | | |
198 | | /* |
199 | | * In normal operation we would have all three pages (source, dest, and |
200 | | * parent) locked simultaneously; but in WAL replay it should be safe to |
201 | | * update them one at a time, as long as we do it in the right order. |
202 | | */ |
203 | | |
204 | | /* Insert tuples on the dest page (do first, so redirect is valid) */ |
205 | 0 | if (xldata->newPage) |
206 | 0 | { |
207 | 0 | buffer = XLogInitBufferForRedo(record, 1); |
208 | 0 | SpGistInitBuffer(buffer, |
209 | 0 | SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); |
210 | 0 | action = BLK_NEEDS_REDO; |
211 | 0 | } |
212 | 0 | else |
213 | 0 | action = XLogReadBufferForRedo(record, 1, &buffer); |
214 | |
|
215 | 0 | if (action == BLK_NEEDS_REDO) |
216 | 0 | { |
217 | 0 | int i; |
218 | |
|
219 | 0 | page = BufferGetPage(buffer); |
220 | |
|
221 | 0 | for (i = 0; i < nInsert; i++) |
222 | 0 | { |
223 | 0 | char *leafTuple; |
224 | 0 | SpGistLeafTupleData leafTupleHdr; |
225 | | |
226 | | /* |
227 | | * the tuples are not aligned, so must copy to access the size |
228 | | * field. |
229 | | */ |
230 | 0 | leafTuple = ptr; |
231 | 0 | memcpy(&leafTupleHdr, leafTuple, |
232 | 0 | sizeof(SpGistLeafTupleData)); |
233 | |
|
234 | 0 | addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, toInsert[i]); |
235 | 0 | ptr += leafTupleHdr.size; |
236 | 0 | } |
237 | |
|
238 | 0 | PageSetLSN(page, lsn); |
239 | 0 | MarkBufferDirty(buffer); |
240 | 0 | } |
241 | 0 | if (BufferIsValid(buffer)) |
242 | 0 | UnlockReleaseBuffer(buffer); |
243 | | |
244 | | /* Delete tuples from the source page, inserting a redirection pointer */ |
245 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
246 | 0 | { |
247 | 0 | page = BufferGetPage(buffer); |
248 | |
|
249 | 0 | spgPageIndexMultiDelete(&state, page, toDelete, xldata->nMoves, |
250 | 0 | state.isBuild ? SPGIST_PLACEHOLDER : SPGIST_REDIRECT, |
251 | 0 | SPGIST_PLACEHOLDER, |
252 | 0 | blknoDst, |
253 | 0 | toInsert[nInsert - 1]); |
254 | |
|
255 | 0 | PageSetLSN(page, lsn); |
256 | 0 | MarkBufferDirty(buffer); |
257 | 0 | } |
258 | 0 | if (BufferIsValid(buffer)) |
259 | 0 | UnlockReleaseBuffer(buffer); |
260 | | |
261 | | /* And update the parent downlink */ |
262 | 0 | if (XLogReadBufferForRedo(record, 2, &buffer) == BLK_NEEDS_REDO) |
263 | 0 | { |
264 | 0 | SpGistInnerTuple tuple; |
265 | |
|
266 | 0 | page = BufferGetPage(buffer); |
267 | |
|
268 | 0 | tuple = (SpGistInnerTuple) PageGetItem(page, |
269 | 0 | PageGetItemId(page, xldata->offnumParent)); |
270 | |
|
271 | 0 | spgUpdateNodeLink(tuple, xldata->nodeI, |
272 | 0 | blknoDst, toInsert[nInsert - 1]); |
273 | |
|
274 | 0 | PageSetLSN(page, lsn); |
275 | 0 | MarkBufferDirty(buffer); |
276 | 0 | } |
277 | 0 | if (BufferIsValid(buffer)) |
278 | 0 | UnlockReleaseBuffer(buffer); |
279 | 0 | } |
280 | | |
281 | | static void |
282 | | spgRedoAddNode(XLogReaderState *record) |
283 | 0 | { |
284 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
285 | 0 | char *ptr = XLogRecGetData(record); |
286 | 0 | spgxlogAddNode *xldata = (spgxlogAddNode *) ptr; |
287 | 0 | char *innerTuple; |
288 | 0 | SpGistInnerTupleData innerTupleHdr; |
289 | 0 | SpGistState state; |
290 | 0 | Buffer buffer; |
291 | 0 | Page page; |
292 | 0 | XLogRedoAction action; |
293 | |
|
294 | 0 | ptr += sizeof(spgxlogAddNode); |
295 | 0 | innerTuple = ptr; |
296 | | /* the tuple is unaligned, so make a copy to access its header */ |
297 | 0 | memcpy(&innerTupleHdr, innerTuple, sizeof(SpGistInnerTupleData)); |
298 | |
|
299 | 0 | fillFakeState(&state, xldata->stateSrc); |
300 | |
|
301 | 0 | if (!XLogRecHasBlockRef(record, 1)) |
302 | 0 | { |
303 | | /* update in place */ |
304 | 0 | Assert(xldata->parentBlk == -1); |
305 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
306 | 0 | { |
307 | 0 | page = BufferGetPage(buffer); |
308 | |
|
309 | 0 | PageIndexTupleDelete(page, xldata->offnum); |
310 | 0 | if (PageAddItem(page, innerTuple, innerTupleHdr.size, |
311 | 0 | xldata->offnum, |
312 | 0 | false, false) != xldata->offnum) |
313 | 0 | elog(ERROR, "failed to add item of size %u to SPGiST index page", |
314 | 0 | innerTupleHdr.size); |
315 | | |
316 | 0 | PageSetLSN(page, lsn); |
317 | 0 | MarkBufferDirty(buffer); |
318 | 0 | } |
319 | 0 | if (BufferIsValid(buffer)) |
320 | 0 | UnlockReleaseBuffer(buffer); |
321 | 0 | } |
322 | 0 | else |
323 | 0 | { |
324 | 0 | BlockNumber blkno; |
325 | 0 | BlockNumber blknoNew; |
326 | |
|
327 | 0 | XLogRecGetBlockTag(record, 0, NULL, NULL, &blkno); |
328 | 0 | XLogRecGetBlockTag(record, 1, NULL, NULL, &blknoNew); |
329 | | |
330 | | /* |
331 | | * In normal operation we would have all three pages (source, dest, |
332 | | * and parent) locked simultaneously; but in WAL replay it should be |
333 | | * safe to update them one at a time, as long as we do it in the right |
334 | | * order. We must insert the new tuple before replacing the old tuple |
335 | | * with the redirect tuple. |
336 | | */ |
337 | | |
338 | | /* Install new tuple first so redirect is valid */ |
339 | 0 | if (xldata->newPage) |
340 | 0 | { |
341 | | /* AddNode is not used for nulls pages */ |
342 | 0 | buffer = XLogInitBufferForRedo(record, 1); |
343 | 0 | SpGistInitBuffer(buffer, 0); |
344 | 0 | action = BLK_NEEDS_REDO; |
345 | 0 | } |
346 | 0 | else |
347 | 0 | action = XLogReadBufferForRedo(record, 1, &buffer); |
348 | 0 | if (action == BLK_NEEDS_REDO) |
349 | 0 | { |
350 | 0 | page = BufferGetPage(buffer); |
351 | |
|
352 | 0 | addOrReplaceTuple(page, innerTuple, innerTupleHdr.size, xldata->offnumNew); |
353 | | |
354 | | /* |
355 | | * If parent is in this same page, update it now. |
356 | | */ |
357 | 0 | if (xldata->parentBlk == 1) |
358 | 0 | { |
359 | 0 | SpGistInnerTuple parentTuple; |
360 | |
|
361 | 0 | parentTuple = (SpGistInnerTuple) PageGetItem(page, |
362 | 0 | PageGetItemId(page, xldata->offnumParent)); |
363 | |
|
364 | 0 | spgUpdateNodeLink(parentTuple, xldata->nodeI, |
365 | 0 | blknoNew, xldata->offnumNew); |
366 | 0 | } |
367 | 0 | PageSetLSN(page, lsn); |
368 | 0 | MarkBufferDirty(buffer); |
369 | 0 | } |
370 | 0 | if (BufferIsValid(buffer)) |
371 | 0 | UnlockReleaseBuffer(buffer); |
372 | | |
373 | | /* Delete old tuple, replacing it with redirect or placeholder tuple */ |
374 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
375 | 0 | { |
376 | 0 | SpGistDeadTuple dt; |
377 | |
|
378 | 0 | page = BufferGetPage(buffer); |
379 | |
|
380 | 0 | if (state.isBuild) |
381 | 0 | dt = spgFormDeadTuple(&state, SPGIST_PLACEHOLDER, |
382 | 0 | InvalidBlockNumber, |
383 | 0 | InvalidOffsetNumber); |
384 | 0 | else |
385 | 0 | dt = spgFormDeadTuple(&state, SPGIST_REDIRECT, |
386 | 0 | blknoNew, |
387 | 0 | xldata->offnumNew); |
388 | |
|
389 | 0 | PageIndexTupleDelete(page, xldata->offnum); |
390 | 0 | if (PageAddItem(page, dt, dt->size, |
391 | 0 | xldata->offnum, |
392 | 0 | false, false) != xldata->offnum) |
393 | 0 | elog(ERROR, "failed to add item of size %u to SPGiST index page", |
394 | 0 | dt->size); |
395 | | |
396 | 0 | if (state.isBuild) |
397 | 0 | SpGistPageGetOpaque(page)->nPlaceholder++; |
398 | 0 | else |
399 | 0 | SpGistPageGetOpaque(page)->nRedirection++; |
400 | | |
401 | | /* |
402 | | * If parent is in this same page, update it now. |
403 | | */ |
404 | 0 | if (xldata->parentBlk == 0) |
405 | 0 | { |
406 | 0 | SpGistInnerTuple parentTuple; |
407 | |
|
408 | 0 | parentTuple = (SpGistInnerTuple) PageGetItem(page, |
409 | 0 | PageGetItemId(page, xldata->offnumParent)); |
410 | |
|
411 | 0 | spgUpdateNodeLink(parentTuple, xldata->nodeI, |
412 | 0 | blknoNew, xldata->offnumNew); |
413 | 0 | } |
414 | 0 | PageSetLSN(page, lsn); |
415 | 0 | MarkBufferDirty(buffer); |
416 | 0 | } |
417 | 0 | if (BufferIsValid(buffer)) |
418 | 0 | UnlockReleaseBuffer(buffer); |
419 | | |
420 | | /* |
421 | | * Update parent downlink (if we didn't do it as part of the source or |
422 | | * destination page update already). |
423 | | */ |
424 | 0 | if (xldata->parentBlk == 2) |
425 | 0 | { |
426 | 0 | if (XLogReadBufferForRedo(record, 2, &buffer) == BLK_NEEDS_REDO) |
427 | 0 | { |
428 | 0 | SpGistInnerTuple parentTuple; |
429 | |
|
430 | 0 | page = BufferGetPage(buffer); |
431 | |
|
432 | 0 | parentTuple = (SpGistInnerTuple) PageGetItem(page, |
433 | 0 | PageGetItemId(page, xldata->offnumParent)); |
434 | |
|
435 | 0 | spgUpdateNodeLink(parentTuple, xldata->nodeI, |
436 | 0 | blknoNew, xldata->offnumNew); |
437 | |
|
438 | 0 | PageSetLSN(page, lsn); |
439 | 0 | MarkBufferDirty(buffer); |
440 | 0 | } |
441 | 0 | if (BufferIsValid(buffer)) |
442 | 0 | UnlockReleaseBuffer(buffer); |
443 | 0 | } |
444 | 0 | } |
445 | 0 | } |
446 | | |
447 | | static void |
448 | | spgRedoSplitTuple(XLogReaderState *record) |
449 | 0 | { |
450 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
451 | 0 | char *ptr = XLogRecGetData(record); |
452 | 0 | spgxlogSplitTuple *xldata = (spgxlogSplitTuple *) ptr; |
453 | 0 | char *prefixTuple; |
454 | 0 | SpGistInnerTupleData prefixTupleHdr; |
455 | 0 | char *postfixTuple; |
456 | 0 | SpGistInnerTupleData postfixTupleHdr; |
457 | 0 | Buffer buffer; |
458 | 0 | Page page; |
459 | 0 | XLogRedoAction action; |
460 | |
|
461 | 0 | ptr += sizeof(spgxlogSplitTuple); |
462 | 0 | prefixTuple = ptr; |
463 | | /* the prefix tuple is unaligned, so make a copy to access its header */ |
464 | 0 | memcpy(&prefixTupleHdr, prefixTuple, sizeof(SpGistInnerTupleData)); |
465 | 0 | ptr += prefixTupleHdr.size; |
466 | 0 | postfixTuple = ptr; |
467 | | /* postfix tuple is also unaligned */ |
468 | 0 | memcpy(&postfixTupleHdr, postfixTuple, sizeof(SpGistInnerTupleData)); |
469 | | |
470 | | /* |
471 | | * In normal operation we would have both pages locked simultaneously; but |
472 | | * in WAL replay it should be safe to update them one at a time, as long |
473 | | * as we do it in the right order. |
474 | | */ |
475 | | |
476 | | /* insert postfix tuple first to avoid dangling link */ |
477 | 0 | if (!xldata->postfixBlkSame) |
478 | 0 | { |
479 | 0 | if (xldata->newPage) |
480 | 0 | { |
481 | 0 | buffer = XLogInitBufferForRedo(record, 1); |
482 | | /* SplitTuple is not used for nulls pages */ |
483 | 0 | SpGistInitBuffer(buffer, 0); |
484 | 0 | action = BLK_NEEDS_REDO; |
485 | 0 | } |
486 | 0 | else |
487 | 0 | action = XLogReadBufferForRedo(record, 1, &buffer); |
488 | 0 | if (action == BLK_NEEDS_REDO) |
489 | 0 | { |
490 | 0 | page = BufferGetPage(buffer); |
491 | |
|
492 | 0 | addOrReplaceTuple(page, postfixTuple, postfixTupleHdr.size, xldata->offnumPostfix); |
493 | |
|
494 | 0 | PageSetLSN(page, lsn); |
495 | 0 | MarkBufferDirty(buffer); |
496 | 0 | } |
497 | 0 | if (BufferIsValid(buffer)) |
498 | 0 | UnlockReleaseBuffer(buffer); |
499 | 0 | } |
500 | | |
501 | | /* now handle the original page */ |
502 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
503 | 0 | { |
504 | 0 | page = BufferGetPage(buffer); |
505 | |
|
506 | 0 | PageIndexTupleDelete(page, xldata->offnumPrefix); |
507 | 0 | if (PageAddItem(page, prefixTuple, prefixTupleHdr.size, |
508 | 0 | xldata->offnumPrefix, false, false) != xldata->offnumPrefix) |
509 | 0 | elog(ERROR, "failed to add item of size %u to SPGiST index page", |
510 | 0 | prefixTupleHdr.size); |
511 | | |
512 | 0 | if (xldata->postfixBlkSame) |
513 | 0 | addOrReplaceTuple(page, postfixTuple, postfixTupleHdr.size, xldata->offnumPostfix); |
514 | |
|
515 | 0 | PageSetLSN(page, lsn); |
516 | 0 | MarkBufferDirty(buffer); |
517 | 0 | } |
518 | 0 | if (BufferIsValid(buffer)) |
519 | 0 | UnlockReleaseBuffer(buffer); |
520 | 0 | } |
521 | | |
522 | | static void |
523 | | spgRedoPickSplit(XLogReaderState *record) |
524 | 0 | { |
525 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
526 | 0 | char *ptr = XLogRecGetData(record); |
527 | 0 | spgxlogPickSplit *xldata = (spgxlogPickSplit *) ptr; |
528 | 0 | char *innerTuple; |
529 | 0 | SpGistInnerTupleData innerTupleHdr; |
530 | 0 | SpGistState state; |
531 | 0 | OffsetNumber *toDelete; |
532 | 0 | OffsetNumber *toInsert; |
533 | 0 | uint8 *leafPageSelect; |
534 | 0 | Buffer srcBuffer; |
535 | 0 | Buffer destBuffer; |
536 | 0 | Buffer innerBuffer; |
537 | 0 | Page srcPage; |
538 | 0 | Page destPage; |
539 | 0 | Page page; |
540 | 0 | int i; |
541 | 0 | BlockNumber blknoInner; |
542 | 0 | XLogRedoAction action; |
543 | |
|
544 | 0 | XLogRecGetBlockTag(record, 2, NULL, NULL, &blknoInner); |
545 | |
|
546 | 0 | fillFakeState(&state, xldata->stateSrc); |
547 | |
|
548 | 0 | ptr += SizeOfSpgxlogPickSplit; |
549 | 0 | toDelete = (OffsetNumber *) ptr; |
550 | 0 | ptr += sizeof(OffsetNumber) * xldata->nDelete; |
551 | 0 | toInsert = (OffsetNumber *) ptr; |
552 | 0 | ptr += sizeof(OffsetNumber) * xldata->nInsert; |
553 | 0 | leafPageSelect = (uint8 *) ptr; |
554 | 0 | ptr += sizeof(uint8) * xldata->nInsert; |
555 | |
|
556 | 0 | innerTuple = ptr; |
557 | | /* the inner tuple is unaligned, so make a copy to access its header */ |
558 | 0 | memcpy(&innerTupleHdr, innerTuple, sizeof(SpGistInnerTupleData)); |
559 | 0 | ptr += innerTupleHdr.size; |
560 | | |
561 | | /* now ptr points to the list of leaf tuples */ |
562 | |
|
563 | 0 | if (xldata->isRootSplit) |
564 | 0 | { |
565 | | /* when splitting root, we touch it only in the guise of new inner */ |
566 | 0 | srcBuffer = InvalidBuffer; |
567 | 0 | srcPage = NULL; |
568 | 0 | } |
569 | 0 | else if (xldata->initSrc) |
570 | 0 | { |
571 | | /* just re-init the source page */ |
572 | 0 | srcBuffer = XLogInitBufferForRedo(record, 0); |
573 | 0 | srcPage = BufferGetPage(srcBuffer); |
574 | |
|
575 | 0 | SpGistInitBuffer(srcBuffer, |
576 | 0 | SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); |
577 | | /* don't update LSN etc till we're done with it */ |
578 | 0 | } |
579 | 0 | else |
580 | 0 | { |
581 | | /* |
582 | | * Delete the specified tuples from source page. (In case we're in |
583 | | * Hot Standby, we need to hold lock on the page till we're done |
584 | | * inserting leaf tuples and the new inner tuple, else the added |
585 | | * redirect tuple will be a dangling link.) |
586 | | */ |
587 | 0 | srcPage = NULL; |
588 | 0 | if (XLogReadBufferForRedo(record, 0, &srcBuffer) == BLK_NEEDS_REDO) |
589 | 0 | { |
590 | 0 | srcPage = BufferGetPage(srcBuffer); |
591 | | |
592 | | /* |
593 | | * We have it a bit easier here than in doPickSplit(), because we |
594 | | * know the inner tuple's location already, so we can inject the |
595 | | * correct redirection tuple now. |
596 | | */ |
597 | 0 | if (!state.isBuild) |
598 | 0 | spgPageIndexMultiDelete(&state, srcPage, |
599 | 0 | toDelete, xldata->nDelete, |
600 | 0 | SPGIST_REDIRECT, |
601 | 0 | SPGIST_PLACEHOLDER, |
602 | 0 | blknoInner, |
603 | 0 | xldata->offnumInner); |
604 | 0 | else |
605 | 0 | spgPageIndexMultiDelete(&state, srcPage, |
606 | 0 | toDelete, xldata->nDelete, |
607 | 0 | SPGIST_PLACEHOLDER, |
608 | 0 | SPGIST_PLACEHOLDER, |
609 | 0 | InvalidBlockNumber, |
610 | 0 | InvalidOffsetNumber); |
611 | | |
612 | | /* don't update LSN etc till we're done with it */ |
613 | 0 | } |
614 | 0 | } |
615 | | |
616 | | /* try to access dest page if any */ |
617 | 0 | if (!XLogRecHasBlockRef(record, 1)) |
618 | 0 | { |
619 | 0 | destBuffer = InvalidBuffer; |
620 | 0 | destPage = NULL; |
621 | 0 | } |
622 | 0 | else if (xldata->initDest) |
623 | 0 | { |
624 | | /* just re-init the dest page */ |
625 | 0 | destBuffer = XLogInitBufferForRedo(record, 1); |
626 | 0 | destPage = BufferGetPage(destBuffer); |
627 | |
|
628 | 0 | SpGistInitBuffer(destBuffer, |
629 | 0 | SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); |
630 | | /* don't update LSN etc till we're done with it */ |
631 | 0 | } |
632 | 0 | else |
633 | 0 | { |
634 | | /* |
635 | | * We could probably release the page lock immediately in the |
636 | | * full-page-image case, but for safety let's hold it till later. |
637 | | */ |
638 | 0 | if (XLogReadBufferForRedo(record, 1, &destBuffer) == BLK_NEEDS_REDO) |
639 | 0 | destPage = BufferGetPage(destBuffer); |
640 | 0 | else |
641 | 0 | destPage = NULL; /* don't do any page updates */ |
642 | 0 | } |
643 | | |
644 | | /* restore leaf tuples to src and/or dest page */ |
645 | 0 | for (i = 0; i < xldata->nInsert; i++) |
646 | 0 | { |
647 | 0 | char *leafTuple; |
648 | 0 | SpGistLeafTupleData leafTupleHdr; |
649 | | |
650 | | /* the tuples are not aligned, so must copy to access the size field. */ |
651 | 0 | leafTuple = ptr; |
652 | 0 | memcpy(&leafTupleHdr, leafTuple, sizeof(SpGistLeafTupleData)); |
653 | 0 | ptr += leafTupleHdr.size; |
654 | |
|
655 | 0 | page = leafPageSelect[i] ? destPage : srcPage; |
656 | 0 | if (page == NULL) |
657 | 0 | continue; /* no need to touch this page */ |
658 | | |
659 | 0 | addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, toInsert[i]); |
660 | 0 | } |
661 | | |
662 | | /* Now update src and dest page LSNs if needed */ |
663 | 0 | if (srcPage != NULL) |
664 | 0 | { |
665 | 0 | PageSetLSN(srcPage, lsn); |
666 | 0 | MarkBufferDirty(srcBuffer); |
667 | 0 | } |
668 | 0 | if (destPage != NULL) |
669 | 0 | { |
670 | 0 | PageSetLSN(destPage, lsn); |
671 | 0 | MarkBufferDirty(destBuffer); |
672 | 0 | } |
673 | | |
674 | | /* restore new inner tuple */ |
675 | 0 | if (xldata->initInner) |
676 | 0 | { |
677 | 0 | innerBuffer = XLogInitBufferForRedo(record, 2); |
678 | 0 | SpGistInitBuffer(innerBuffer, (xldata->storesNulls ? SPGIST_NULLS : 0)); |
679 | 0 | action = BLK_NEEDS_REDO; |
680 | 0 | } |
681 | 0 | else |
682 | 0 | action = XLogReadBufferForRedo(record, 2, &innerBuffer); |
683 | |
|
684 | 0 | if (action == BLK_NEEDS_REDO) |
685 | 0 | { |
686 | 0 | page = BufferGetPage(innerBuffer); |
687 | |
|
688 | 0 | addOrReplaceTuple(page, innerTuple, innerTupleHdr.size, xldata->offnumInner); |
689 | | |
690 | | /* if inner is also parent, update link while we're here */ |
691 | 0 | if (xldata->innerIsParent) |
692 | 0 | { |
693 | 0 | SpGistInnerTuple parent; |
694 | |
|
695 | 0 | parent = (SpGistInnerTuple) PageGetItem(page, |
696 | 0 | PageGetItemId(page, xldata->offnumParent)); |
697 | 0 | spgUpdateNodeLink(parent, xldata->nodeI, |
698 | 0 | blknoInner, xldata->offnumInner); |
699 | 0 | } |
700 | |
|
701 | 0 | PageSetLSN(page, lsn); |
702 | 0 | MarkBufferDirty(innerBuffer); |
703 | 0 | } |
704 | 0 | if (BufferIsValid(innerBuffer)) |
705 | 0 | UnlockReleaseBuffer(innerBuffer); |
706 | | |
707 | | /* |
708 | | * Now we can release the leaf-page locks. It's okay to do this before |
709 | | * updating the parent downlink. |
710 | | */ |
711 | 0 | if (BufferIsValid(srcBuffer)) |
712 | 0 | UnlockReleaseBuffer(srcBuffer); |
713 | 0 | if (BufferIsValid(destBuffer)) |
714 | 0 | UnlockReleaseBuffer(destBuffer); |
715 | | |
716 | | /* update parent downlink, unless we did it above */ |
717 | 0 | if (XLogRecHasBlockRef(record, 3)) |
718 | 0 | { |
719 | 0 | Buffer parentBuffer; |
720 | |
|
721 | 0 | if (XLogReadBufferForRedo(record, 3, &parentBuffer) == BLK_NEEDS_REDO) |
722 | 0 | { |
723 | 0 | SpGistInnerTuple parent; |
724 | |
|
725 | 0 | page = BufferGetPage(parentBuffer); |
726 | |
|
727 | 0 | parent = (SpGistInnerTuple) PageGetItem(page, |
728 | 0 | PageGetItemId(page, xldata->offnumParent)); |
729 | 0 | spgUpdateNodeLink(parent, xldata->nodeI, |
730 | 0 | blknoInner, xldata->offnumInner); |
731 | |
|
732 | 0 | PageSetLSN(page, lsn); |
733 | 0 | MarkBufferDirty(parentBuffer); |
734 | 0 | } |
735 | 0 | if (BufferIsValid(parentBuffer)) |
736 | 0 | UnlockReleaseBuffer(parentBuffer); |
737 | 0 | } |
738 | 0 | else |
739 | 0 | Assert(xldata->innerIsParent || xldata->isRootSplit); |
740 | 0 | } |
741 | | |
742 | | static void |
743 | | spgRedoVacuumLeaf(XLogReaderState *record) |
744 | 0 | { |
745 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
746 | 0 | char *ptr = XLogRecGetData(record); |
747 | 0 | spgxlogVacuumLeaf *xldata = (spgxlogVacuumLeaf *) ptr; |
748 | 0 | OffsetNumber *toDead; |
749 | 0 | OffsetNumber *toPlaceholder; |
750 | 0 | OffsetNumber *moveSrc; |
751 | 0 | OffsetNumber *moveDest; |
752 | 0 | OffsetNumber *chainSrc; |
753 | 0 | OffsetNumber *chainDest; |
754 | 0 | SpGistState state; |
755 | 0 | Buffer buffer; |
756 | 0 | Page page; |
757 | 0 | int i; |
758 | |
|
759 | 0 | fillFakeState(&state, xldata->stateSrc); |
760 | |
|
761 | 0 | ptr += SizeOfSpgxlogVacuumLeaf; |
762 | 0 | toDead = (OffsetNumber *) ptr; |
763 | 0 | ptr += sizeof(OffsetNumber) * xldata->nDead; |
764 | 0 | toPlaceholder = (OffsetNumber *) ptr; |
765 | 0 | ptr += sizeof(OffsetNumber) * xldata->nPlaceholder; |
766 | 0 | moveSrc = (OffsetNumber *) ptr; |
767 | 0 | ptr += sizeof(OffsetNumber) * xldata->nMove; |
768 | 0 | moveDest = (OffsetNumber *) ptr; |
769 | 0 | ptr += sizeof(OffsetNumber) * xldata->nMove; |
770 | 0 | chainSrc = (OffsetNumber *) ptr; |
771 | 0 | ptr += sizeof(OffsetNumber) * xldata->nChain; |
772 | 0 | chainDest = (OffsetNumber *) ptr; |
773 | |
|
774 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
775 | 0 | { |
776 | 0 | page = BufferGetPage(buffer); |
777 | |
|
778 | 0 | spgPageIndexMultiDelete(&state, page, |
779 | 0 | toDead, xldata->nDead, |
780 | 0 | SPGIST_DEAD, SPGIST_DEAD, |
781 | 0 | InvalidBlockNumber, |
782 | 0 | InvalidOffsetNumber); |
783 | |
|
784 | 0 | spgPageIndexMultiDelete(&state, page, |
785 | 0 | toPlaceholder, xldata->nPlaceholder, |
786 | 0 | SPGIST_PLACEHOLDER, SPGIST_PLACEHOLDER, |
787 | 0 | InvalidBlockNumber, |
788 | 0 | InvalidOffsetNumber); |
789 | | |
790 | | /* see comments in vacuumLeafPage() */ |
791 | 0 | for (i = 0; i < xldata->nMove; i++) |
792 | 0 | { |
793 | 0 | ItemId idSrc = PageGetItemId(page, moveSrc[i]); |
794 | 0 | ItemId idDest = PageGetItemId(page, moveDest[i]); |
795 | 0 | ItemIdData tmp; |
796 | |
|
797 | 0 | tmp = *idSrc; |
798 | 0 | *idSrc = *idDest; |
799 | 0 | *idDest = tmp; |
800 | 0 | } |
801 | |
|
802 | 0 | spgPageIndexMultiDelete(&state, page, |
803 | 0 | moveSrc, xldata->nMove, |
804 | 0 | SPGIST_PLACEHOLDER, SPGIST_PLACEHOLDER, |
805 | 0 | InvalidBlockNumber, |
806 | 0 | InvalidOffsetNumber); |
807 | |
|
808 | 0 | for (i = 0; i < xldata->nChain; i++) |
809 | 0 | { |
810 | 0 | SpGistLeafTuple lt; |
811 | |
|
812 | 0 | lt = (SpGistLeafTuple) PageGetItem(page, |
813 | 0 | PageGetItemId(page, chainSrc[i])); |
814 | 0 | Assert(lt->tupstate == SPGIST_LIVE); |
815 | 0 | SGLT_SET_NEXTOFFSET(lt, chainDest[i]); |
816 | 0 | } |
817 | |
|
818 | 0 | PageSetLSN(page, lsn); |
819 | 0 | MarkBufferDirty(buffer); |
820 | 0 | } |
821 | 0 | if (BufferIsValid(buffer)) |
822 | 0 | UnlockReleaseBuffer(buffer); |
823 | 0 | } |
824 | | |
825 | | static void |
826 | | spgRedoVacuumRoot(XLogReaderState *record) |
827 | 0 | { |
828 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
829 | 0 | char *ptr = XLogRecGetData(record); |
830 | 0 | spgxlogVacuumRoot *xldata = (spgxlogVacuumRoot *) ptr; |
831 | 0 | OffsetNumber *toDelete; |
832 | 0 | Buffer buffer; |
833 | 0 | Page page; |
834 | |
|
835 | 0 | toDelete = xldata->offsets; |
836 | |
|
837 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
838 | 0 | { |
839 | 0 | page = BufferGetPage(buffer); |
840 | | |
841 | | /* The tuple numbers are in order */ |
842 | 0 | PageIndexMultiDelete(page, toDelete, xldata->nDelete); |
843 | |
|
844 | 0 | PageSetLSN(page, lsn); |
845 | 0 | MarkBufferDirty(buffer); |
846 | 0 | } |
847 | 0 | if (BufferIsValid(buffer)) |
848 | 0 | UnlockReleaseBuffer(buffer); |
849 | 0 | } |
850 | | |
851 | | static void |
852 | | spgRedoVacuumRedirect(XLogReaderState *record) |
853 | 0 | { |
854 | 0 | XLogRecPtr lsn = record->EndRecPtr; |
855 | 0 | char *ptr = XLogRecGetData(record); |
856 | 0 | spgxlogVacuumRedirect *xldata = (spgxlogVacuumRedirect *) ptr; |
857 | 0 | OffsetNumber *itemToPlaceholder; |
858 | 0 | Buffer buffer; |
859 | |
|
860 | 0 | itemToPlaceholder = xldata->offsets; |
861 | | |
862 | | /* |
863 | | * If any redirection tuples are being removed, make sure there are no |
864 | | * live Hot Standby transactions that might need to see them. |
865 | | */ |
866 | 0 | if (InHotStandby) |
867 | 0 | { |
868 | 0 | RelFileLocator locator; |
869 | |
|
870 | 0 | XLogRecGetBlockTag(record, 0, &locator, NULL, NULL); |
871 | 0 | ResolveRecoveryConflictWithSnapshot(xldata->snapshotConflictHorizon, |
872 | 0 | xldata->isCatalogRel, |
873 | 0 | locator); |
874 | 0 | } |
875 | |
|
876 | 0 | if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) |
877 | 0 | { |
878 | 0 | Page page = BufferGetPage(buffer); |
879 | 0 | SpGistPageOpaque opaque = SpGistPageGetOpaque(page); |
880 | 0 | int i; |
881 | | |
882 | | /* Convert redirect pointers to plain placeholders */ |
883 | 0 | for (i = 0; i < xldata->nToPlaceholder; i++) |
884 | 0 | { |
885 | 0 | SpGistDeadTuple dt; |
886 | |
|
887 | 0 | dt = (SpGistDeadTuple) PageGetItem(page, |
888 | 0 | PageGetItemId(page, itemToPlaceholder[i])); |
889 | 0 | Assert(dt->tupstate == SPGIST_REDIRECT); |
890 | 0 | dt->tupstate = SPGIST_PLACEHOLDER; |
891 | 0 | ItemPointerSetInvalid(&dt->pointer); |
892 | 0 | } |
893 | |
|
894 | 0 | Assert(opaque->nRedirection >= xldata->nToPlaceholder); |
895 | 0 | opaque->nRedirection -= xldata->nToPlaceholder; |
896 | 0 | opaque->nPlaceholder += xldata->nToPlaceholder; |
897 | | |
898 | | /* Remove placeholder tuples at end of page */ |
899 | 0 | if (xldata->firstPlaceholder != InvalidOffsetNumber) |
900 | 0 | { |
901 | 0 | int max = PageGetMaxOffsetNumber(page); |
902 | 0 | OffsetNumber *toDelete; |
903 | |
|
904 | 0 | toDelete = palloc_array(OffsetNumber, max); |
905 | |
|
906 | 0 | for (i = xldata->firstPlaceholder; i <= max; i++) |
907 | 0 | toDelete[i - xldata->firstPlaceholder] = i; |
908 | |
|
909 | 0 | i = max - xldata->firstPlaceholder + 1; |
910 | 0 | Assert(opaque->nPlaceholder >= i); |
911 | 0 | opaque->nPlaceholder -= i; |
912 | | |
913 | | /* The array is sorted, so can use PageIndexMultiDelete */ |
914 | 0 | PageIndexMultiDelete(page, toDelete, i); |
915 | |
|
916 | 0 | pfree(toDelete); |
917 | 0 | } |
918 | |
|
919 | 0 | PageSetLSN(page, lsn); |
920 | 0 | MarkBufferDirty(buffer); |
921 | 0 | } |
922 | 0 | if (BufferIsValid(buffer)) |
923 | 0 | UnlockReleaseBuffer(buffer); |
924 | 0 | } |
925 | | |
926 | | void |
927 | | spg_redo(XLogReaderState *record) |
928 | 0 | { |
929 | 0 | uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; |
930 | 0 | MemoryContext oldCxt; |
931 | |
|
932 | 0 | oldCxt = MemoryContextSwitchTo(opCtx); |
933 | 0 | switch (info) |
934 | 0 | { |
935 | 0 | case XLOG_SPGIST_ADD_LEAF: |
936 | 0 | spgRedoAddLeaf(record); |
937 | 0 | break; |
938 | 0 | case XLOG_SPGIST_MOVE_LEAFS: |
939 | 0 | spgRedoMoveLeafs(record); |
940 | 0 | break; |
941 | 0 | case XLOG_SPGIST_ADD_NODE: |
942 | 0 | spgRedoAddNode(record); |
943 | 0 | break; |
944 | 0 | case XLOG_SPGIST_SPLIT_TUPLE: |
945 | 0 | spgRedoSplitTuple(record); |
946 | 0 | break; |
947 | 0 | case XLOG_SPGIST_PICKSPLIT: |
948 | 0 | spgRedoPickSplit(record); |
949 | 0 | break; |
950 | 0 | case XLOG_SPGIST_VACUUM_LEAF: |
951 | 0 | spgRedoVacuumLeaf(record); |
952 | 0 | break; |
953 | 0 | case XLOG_SPGIST_VACUUM_ROOT: |
954 | 0 | spgRedoVacuumRoot(record); |
955 | 0 | break; |
956 | 0 | case XLOG_SPGIST_VACUUM_REDIRECT: |
957 | 0 | spgRedoVacuumRedirect(record); |
958 | 0 | break; |
959 | 0 | default: |
960 | 0 | elog(PANIC, "spg_redo: unknown op code %u", info); |
961 | 0 | } |
962 | | |
963 | 0 | MemoryContextSwitchTo(oldCxt); |
964 | 0 | MemoryContextReset(opCtx); |
965 | 0 | } |
966 | | |
967 | | void |
968 | | spg_xlog_startup(void) |
969 | 0 | { |
970 | 0 | opCtx = AllocSetContextCreate(CurrentMemoryContext, |
971 | 0 | "SP-GiST temporary context", |
972 | 0 | ALLOCSET_DEFAULT_SIZES); |
973 | 0 | } |
974 | | |
975 | | void |
976 | | spg_xlog_cleanup(void) |
977 | 0 | { |
978 | 0 | MemoryContextDelete(opCtx); |
979 | 0 | opCtx = NULL; |
980 | 0 | } |
981 | | |
982 | | /* |
983 | | * Mask a SpGist page before performing consistency checks on it. |
984 | | */ |
985 | | void |
986 | | spg_mask(char *pagedata, BlockNumber blkno) |
987 | 0 | { |
988 | 0 | Page page = (Page) pagedata; |
989 | 0 | PageHeader pagehdr = (PageHeader) page; |
990 | |
|
991 | 0 | mask_page_lsn_and_checksum(page); |
992 | |
|
993 | 0 | mask_page_hint_bits(page); |
994 | | |
995 | | /* |
996 | | * Mask the unused space, but only if the page's pd_lower appears to have |
997 | | * been set correctly. |
998 | | */ |
999 | 0 | if (pagehdr->pd_lower >= SizeOfPageHeaderData) |
1000 | 0 | mask_unused_space(page); |
1001 | 0 | } |