diff --git a/src/backend/access/rmgrdesc/Makefile b/src/backend/access/rmgrdesc/Makefile new file mode 100644 index c72a1f2..c0e38fd *** a/src/backend/access/rmgrdesc/Makefile --- b/src/backend/access/rmgrdesc/Makefile *************** subdir = src/backend/access/rmgrdesc *** 8,16 **** top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = brindesc.o clogdesc.o committsdesc.o dbasedesc.o gindesc.o gistdesc.o \ ! hashdesc.o heapdesc.o mxactdesc.o nbtdesc.o relmapdesc.o \ ! replorigindesc.o seqdesc.o smgrdesc.o spgdesc.o \ standbydesc.o tblspcdesc.o xactdesc.o xlogdesc.o include $(top_srcdir)/src/backend/common.mk --- 8,16 ---- top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = brindesc.o clogdesc.o committsdesc.o dbasedesc.o genericdesc.o \ ! gindesc.o gistdesc.o hashdesc.o heapdesc.o mxactdesc.o nbtdesc.o \ ! relmapdesc.o replorigindesc.o seqdesc.o smgrdesc.o spgdesc.o \ standbydesc.o tblspcdesc.o xactdesc.o xlogdesc.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/access/rmgrdesc/genericdesc.c b/src/backend/access/rmgrdesc/genericdesc.c new file mode 100644 index ...3d035c2 *** a/src/backend/access/rmgrdesc/genericdesc.c --- b/src/backend/access/rmgrdesc/genericdesc.c *************** *** 0 **** --- 1,58 ---- + /*------------------------------------------------------------------------- + * + * genericdesc.c + * rmgr descriptor routines for access/transam/generic_xlog.c + * + * + * Portions Copyright (c) 1996-2014, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/access/rmgrdesc/genericdesc.c + * + *------------------------------------------------------------------------- + */ + #include "postgres.h" + + #include "access/generic_xlog.h" + #include "lib/stringinfo.h" + #include "storage/relfilenode.h" + + /* + * Description of generic xlog record: write page regions which this record + * overrides. + */ + void + generic_desc(StringInfo buf, XLogReaderState *record) + { + Pointer ptr = XLogRecGetData(record), + end = ptr + XLogRecGetDataLen(record); + + while (ptr < end) + { + OffsetNumber offset, + length; + + memcpy(&offset, ptr, sizeof(offset)); + ptr += sizeof(offset); + memcpy(&length, ptr, sizeof(length)); + ptr += sizeof(length); + ptr += length; + + if (ptr < end) + appendStringInfo(buf, "offset %u, length %u; ", offset, length); + else + appendStringInfo(buf, "offset %u, length %u", offset, length); + } + + return; + } + + /* + * Identification of generic xlog record: we don't distinguish any subtypes + * inside generic xlog records. + */ + const char * + generic_identify(uint8 info) + { + return "Generic"; + } diff --git a/src/backend/access/transam/Makefile b/src/backend/access/transam/Makefile new file mode 100644 index 94455b2..16fbe47 *** a/src/backend/access/transam/Makefile --- b/src/backend/access/transam/Makefile *************** subdir = src/backend/access/transam *** 12,19 **** top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = clog.o commit_ts.o multixact.o parallel.o rmgr.o slru.o subtrans.o \ ! timeline.o transam.o twophase.o twophase_rmgr.o varsup.o \ xact.o xlog.o xlogarchive.o xlogfuncs.o \ xloginsert.o xlogreader.o xlogutils.o --- 12,19 ---- top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global ! OBJS = clog.o commit_ts.o generic_xlog.o multixact.o parallel.o rmgr.o slru.o \ ! subtrans.o timeline.o transam.o twophase.o twophase_rmgr.o varsup.o \ xact.o xlog.o xlogarchive.o xlogfuncs.o \ xloginsert.o xlogreader.o xlogutils.o diff --git a/src/backend/access/transam/generic_xlog.c b/src/backend/access/transam/generic_xlog.c new file mode 100644 index ...7ca03bf *** a/src/backend/access/transam/generic_xlog.c --- b/src/backend/access/transam/generic_xlog.c *************** *** 0 **** --- 1,510 ---- + /*------------------------------------------------------------------------- + * + * generic_xlog.c + * Implementation of generic xlog records. + * + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/access/transam/generic_xlog.c + * + *------------------------------------------------------------------------- + */ + #include "postgres.h" + + #include "access/generic_xlog.h" + #include "access/xlogutils.h" + #include "miscadmin.h" + #include "utils/memutils.h" + + /*------------------------------------------------------------------------- + * API for construction of generic xlog records + * + * This API allows user to construct generic xlog records which are + * describing difference between pages in a general way. Thus it's useful + * for extensions which provide custom access methods because they can't + * register their own WAL redo routines. + * + * Generic xlog record should be constructed in following steps. + * 1) GenericXLogStart(relation) - start construction of generic xlog + * record for given relation. + * 2) GenericXLogRegister(buffer, isNew) - register one or more buffers + * for generic xlog record. This function returns a copy of the page + * image where modifications can be performed. The second argument + * indicates if block is new and full image should be taken. + * 3) Do modification of page images obtained in previous step. + * 4) GenericXLogFinish() - finish construction of generic xlog record. + * + * Please, note the following points when constructing generic xlog records. + * - No direct modifications of page images are allowed! All modifications + * must be done in copies returned by GenericXLogRegister(). In other + * words the code which makes generic xlog records should never call + * BufferGetPage() function. + * - The xlog record construction can be canceled at any step by calling + * GenericXLogAbort(). All changes made to page images copies will be + * discarded. + * - Registrations of buffers (step 2) and modifications of page images + * (step 3) can be mixed in any sequence. The only restriction is that + * you can only modify page image after registration of corresponding + * buffer. + * - After registration, the buffer also can be unregistered by calling + * GenericXLogUnregister(buffer). In this case the changes made in + * particular page image copy will be discarded. + * - Generic xlog assumes that pages are using standard layout. I.e. all + * information between pd_lower and pd_upper will be discarded. + * - Maximum number of buffers simultaneously registered for generic xlog + * is MAX_GENERIC_XLOG_PAGES. Error will be thrown if this limit + * exceeded. + * - Since you modify copies of page images, GenericXLogStart() doesn't + * start a critical section. Thus, you can do memory allocation, error + * throwing etc between GenericXLogStart() and GenericXLogFinish(). + * Actual critical section is present inside GenericXLogFinish(). + * - GenericXLogFinish() takes care of marking buffers dirty and setting their + * LSNs. You don't need to do this explicitly. + * - For unlogged relations, everything works the same expect there is no + * WAL record produced. Thus, you typically don't need to do any explicit + * checks for unlogged relations. + * - If registered buffer isn't new, generic xlog record contains delta + * between old and new page images. This delta is produced by per byte + * comparison. Current delta mechanist is not effective for data shift + * inside the page. However, it could be improved in further versions. + * - Generic xlog redo function will acquire exclusive locks to buffers + * in the same order they were registered. After redo of all changes + * locks will be released in the same order. + * + * Internally, delta between pages consists of set of fragments. Each + * fragment represents changes made in given region of page. Fragment is + * described as following. + * + * - offset of page region (OffsetNumber) + * - length of page region (OffsetNumber) + * - data - the data to place into described region ('length' number of bytes) + * + * Unchanged regions of page are not represented in the dekta. As a result + * delta can be more compact than full page image. But if unchanged region + * of the page is less than fragment header (offset and length) the delta + * would be bigger than the full page image. For this reason we break fragment + * only if the unchanged region is bigger than MATCH_THRESHOLD. + * + * The worst case for delta size is when we didn't find any unchanged region + * in the page. Then size of delta would be size of page plus size of fragment + * header. + */ + #define FRAGMENT_HEADER_SIZE (2 * sizeof(OffsetNumber)) + #define MATCH_THRESHOLD FRAGMENT_HEADER_SIZE + #define MAX_DELTA_SIZE BLCKSZ + FRAGMENT_HEADER_SIZE + + /* Struct of generic xlog data for single page */ + typedef struct + { + Buffer buffer; /* registered buffer */ + char image[BLCKSZ]; /* copy of page image for modification */ + char data[MAX_DELTA_SIZE]; /* delta between page images */ + int dataLen; /* space consumed in data field */ + bool fullImage; /* are we taking full image of this page? */ + } PageData; + + /* Enum of generic xlog (gxlog) status */ + enum GenericXlogStatus + { + GXLOG_NOT_STARTED, /* gxlog is not started */ + GXLOG_LOGGED, /* gxlog is started for logged relation */ + GXLOG_UNLOGGED /* gxlog is started for unlogged relation */ + }; + + static enum GenericXlogStatus genericXlogStatus = GXLOG_NOT_STARTED; + static PageData pages[MAX_GENERIC_XLOG_PAGES]; + + + static void writeFragment(PageData *pageData, OffsetNumber offset, + OffsetNumber len, Pointer data); + static void writeDelta(PageData *pageData); + static void applyPageRedo(Page page, Pointer data, Size dataSize); + + /* + * Write next fragment into delta. + */ + static void + writeFragment(PageData *pageData, OffsetNumber offset, OffsetNumber length, + Pointer data) + { + Pointer ptr = pageData->data + pageData->dataLen; + + /* Check we have enough of space */ + Assert(pageData->dataLen + sizeof(offset) + + sizeof(length) + length <= sizeof(pageData->data)); + + /* Write fragment data */ + memcpy(ptr, &offset, sizeof(offset)); + ptr += sizeof(offset); + memcpy(ptr, &length, sizeof(length)); + ptr += sizeof(length); + memcpy(ptr, data, length); + ptr += length; + + pageData->dataLen = ptr - pageData->data; + } + + /* + * Make delta for given page. + */ + static void + writeDelta(PageData *pageData) + { + Page page = BufferGetPage(pageData->buffer), + image = (Page) pageData->image; + int i, + fragmentBegin = -1, + fragmentEnd = -1; + uint16 pageLower = ((PageHeader) page)->pd_lower, + pageUpper = ((PageHeader) page)->pd_upper, + imageLower = ((PageHeader) image)->pd_lower, + imageUpper = ((PageHeader) image)->pd_upper; + + for (i = 0; i < BLCKSZ; i++) + { + bool match; + + /* + * Check if bytes in old and new page images matches. We don't care + * about data in unallocated area between pd_lower and pd_upper. Thus + * we assume unallocated area to expand with unmatched bytes. Bytes + * inside unallocated area are assumed to always match. + */ + if (i < pageLower) + { + if (i < imageLower) + match = (page[i] == image[i]); + else + match = false; + } + else if (i >= pageUpper) + { + if (i >= imageUpper) + match = (page[i] == image[i]); + else + match = false; + } + else + { + match = true; + } + + if (match) + { + if (fragmentBegin >= 0) + { + /* Matched byte is potential of fragment. */ + if (fragmentEnd < 0) + fragmentEnd = i; + + /* + * Write next fragment if sequence of matched bytes is longer + * than MATCH_THRESHOLD. + */ + if (i - fragmentEnd >= MATCH_THRESHOLD) + { + writeFragment(pageData, fragmentBegin, + fragmentEnd - fragmentBegin, + page + fragmentBegin); + fragmentBegin = -1; + fragmentEnd = -1; + } + } + } + else + { + /* On unmatched byte, start new fragment if it's not done yet */ + if (fragmentBegin < 0) + fragmentBegin = i; + fragmentEnd = -1; + } + } + + if (fragmentBegin >= 0) + writeFragment(pageData, fragmentBegin, + BLCKSZ - fragmentBegin, + page + fragmentBegin); + + #ifdef WAL_DEBUG + /* + * If xlog debug is enabled then check produced delta. Result of delta + * application to saved image should be the same as current page state. + */ + if (XLOG_DEBUG) + { + char tmp[BLCKSZ]; + memcpy(tmp, image, BLCKSZ); + applyPageRedo(tmp, pageData->data, pageData->dataLen); + if (memcmp(tmp, page, pageLower) + || memcmp(tmp + pageUpper, page + pageUpper, BLCKSZ - pageUpper)) + elog(ERROR, "result of generic xlog apply doesn't match"); + } + #endif + } + + /* + * Start new generic xlog record. + */ + void + GenericXLogStart(Relation relation) + { + int i; + + if (genericXlogStatus != GXLOG_NOT_STARTED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogStart: generic xlog is already started"))); + + genericXlogStatus = RelationNeedsWAL(relation) ? GXLOG_LOGGED : GXLOG_UNLOGGED; + + for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) + { + pages[i].buffer = InvalidBuffer; + } + } + + /* + * Register new buffer for generic xlog record. + */ + Page + GenericXLogRegister(Buffer buffer, bool isNew) + { + int block_id; + + if (genericXlogStatus == GXLOG_NOT_STARTED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogRegister: generic xlog isn't started"))); + + /* Place new buffer to unused slot in array */ + for (block_id = 0; block_id < MAX_GENERIC_XLOG_PAGES; block_id++) + { + if (BufferIsInvalid(pages[block_id].buffer)) + { + pages[block_id].buffer = buffer; + memcpy(pages[block_id].image, BufferGetPage(buffer), BLCKSZ); + pages[block_id].dataLen = 0; + pages[block_id].fullImage = isNew; + return (Page)pages[block_id].image; + } + else if (pages[block_id].buffer == buffer) + { + /* + * Buffer already registered. Just return image which is already + * prepared. + */ + return (Page)pages[block_id].image; + } + } + + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("GenericXLogRegister: maximum number of %d buffers is exceeded", + MAX_GENERIC_XLOG_PAGES))); + + /* keep compiler quiet */ + return NULL; + } + + /* + * Unregister particular buffer for generic xlog record. + */ + void + GenericXLogUnregister(Buffer buffer) + { + int block_id; + + if (genericXlogStatus == GXLOG_NOT_STARTED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogUnregister: generic xlog isn't started"))); + + /* Find block in array to unregister */ + for (block_id = 0; block_id < MAX_GENERIC_XLOG_PAGES; block_id++) + { + if (pages[block_id].buffer == buffer) + { + /* + * Preserve order of pages in array because it could matter for + * concurrency. + */ + memmove(&pages[block_id], &pages[block_id + 1], + (MAX_GENERIC_XLOG_PAGES - block_id - 1) * sizeof(PageData)); + pages[MAX_GENERIC_XLOG_PAGES - 1].buffer = InvalidBuffer; + return; + } + } + + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogUnregister: registered buffer not found"))); + } + + /* + * Put all changes in registered buffers to generic xlog record. + */ + XLogRecPtr + GenericXLogFinish(void) + { + XLogRecPtr lsn = InvalidXLogRecPtr; + int i; + + if (genericXlogStatus == GXLOG_LOGGED) + { + /* Logged relation: make xlog record in critical section. */ + START_CRIT_SECTION(); + XLogBeginInsert(); + + for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) + { + char tmp[BLCKSZ]; + + if (BufferIsInvalid(pages[i].buffer)) + continue; + + /* Swap current and saved page image. */ + memcpy(tmp, pages[i].image, BLCKSZ); + memcpy(pages[i].image, BufferGetPage(pages[i].buffer), BLCKSZ); + memcpy(BufferGetPage(pages[i].buffer), tmp, BLCKSZ); + + if (pages[i].fullImage) + { + /* Full page image doesn't require anything special from us */ + XLogRegisterBuffer(i, pages[i].buffer, REGBUF_FORCE_IMAGE); + } + else + { + /* + * In normal node calculate delta and write use it as data + * associated with this page. + */ + XLogRegisterBuffer(i, pages[i].buffer, REGBUF_STANDARD); + writeDelta(&pages[i]); + XLogRegisterBufData(i, pages[i].data, pages[i].dataLen); + } + } + + /* Insert xlog record */ + lsn = XLogInsert(RM_GENERIC_ID, 0); + + /* Set LSN and make buffers dirty */ + for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) + { + if (BufferIsInvalid(pages[i].buffer)) + continue; + PageSetLSN(BufferGetPage(pages[i].buffer), lsn); + MarkBufferDirty(pages[i].buffer); + } + END_CRIT_SECTION(); + } + else if (genericXlogStatus == GXLOG_UNLOGGED) + { + /* Unlogged relation: skip xlog-related stuff */ + START_CRIT_SECTION(); + for (i = 0; i < MAX_GENERIC_XLOG_PAGES; i++) + { + if (BufferIsInvalid(pages[i].buffer)) + continue; + memcpy(BufferGetPage(pages[i].buffer), pages[i].image, BLCKSZ); + MarkBufferDirty(pages[i].buffer); + } + END_CRIT_SECTION(); + } + else + { + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogFinish: generic xlog isn't started"))); + } + + genericXlogStatus = GXLOG_NOT_STARTED; + + return lsn; + } + + /* + * Abort generic xlog record. + */ + void + GenericXLogAbort(void) + { + if (genericXlogStatus == GXLOG_NOT_STARTED) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("GenericXLogAbort: generic xlog isn't started"))); + + genericXlogStatus = GXLOG_NOT_STARTED; + } + + /* + * Apply delta to given page image. + */ + static void + applyPageRedo(Page page, Pointer data, Size dataSize) + { + Pointer ptr = data, end = data + dataSize; + + while (ptr < end) + { + OffsetNumber offset, + length; + + memcpy(&offset, ptr, sizeof(offset)); + ptr += sizeof(offset); + memcpy(&length, ptr, sizeof(length)); + ptr += sizeof(length); + + memcpy(page + offset, ptr, length); + + ptr += length; + } + } + + /* + * Redo function for generic xlog record. + */ + void + generic_redo(XLogReaderState *record) + { + uint8 block_id; + Buffer buffers[MAX_GENERIC_XLOG_PAGES] = {InvalidBuffer}; + XLogRecPtr lsn = record->EndRecPtr; + + Assert(record->max_block_id < MAX_GENERIC_XLOG_PAGES); + + /* Interate over blocks */ + for (block_id = 0; block_id <= record->max_block_id; block_id++) + { + XLogRedoAction action; + + if (!XLogRecHasBlockRef(record, block_id)) + continue; + + action = XLogReadBufferForRedo(record, block_id, &buffers[block_id]); + + /* Apply redo to given block if needed */ + if (action == BLK_NEEDS_REDO) + { + Pointer blockData; + Size blockDataSize; + Page page; + + page = BufferGetPage(buffers[block_id]); + blockData = XLogRecGetBlockData(record, block_id, &blockDataSize); + applyPageRedo(page, blockData, blockDataSize); + + PageSetLSN(page, lsn); + MarkBufferDirty(buffers[block_id]); + } + } + + /* Changes are done: unlock and release all buffers */ + for (block_id = 0; block_id <= record->max_block_id; block_id++) + { + if (BufferIsValid(buffers[block_id])) + UnlockReleaseBuffer(buffers[block_id]); + } + } diff --git a/src/backend/access/transam/rmgr.c b/src/backend/access/transam/rmgr.c new file mode 100644 index 7c4d773..7b38c16 *** a/src/backend/access/transam/rmgr.c --- b/src/backend/access/transam/rmgr.c *************** *** 11,16 **** --- 11,17 ---- #include "access/commit_ts.h" #include "access/gin.h" #include "access/gist_private.h" + #include "access/generic_xlog.h" #include "access/hash.h" #include "access/heapam_xlog.h" #include "access/brin_xlog.h" diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c new file mode 100644 index 13af485..262deb2 *** a/src/backend/replication/logical/decode.c --- b/src/backend/replication/logical/decode.c *************** LogicalDecodingProcessRecord(LogicalDeco *** 143,148 **** --- 143,149 ---- case RM_BRIN_ID: case RM_COMMIT_TS_ID: case RM_REPLORIGIN_ID: + case RM_GENERIC_ID: /* just deal with xid, and done */ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(record), buf.origptr); diff --git a/src/bin/pg_xlogdump/.gitignore b/src/bin/pg_xlogdump/.gitignore new file mode 100644 index eebaf30..33a1acf *** a/src/bin/pg_xlogdump/.gitignore --- b/src/bin/pg_xlogdump/.gitignore *************** *** 4,9 **** --- 4,10 ---- /clogdesc.c /committsdesc.c /dbasedesc.c + /genericdesc.c /gindesc.c /gistdesc.c /hashdesc.c diff --git a/src/bin/pg_xlogdump/rmgrdesc.c b/src/bin/pg_xlogdump/rmgrdesc.c new file mode 100644 index f9cd395..cff7e59 *** a/src/bin/pg_xlogdump/rmgrdesc.c --- b/src/bin/pg_xlogdump/rmgrdesc.c *************** *** 11,16 **** --- 11,17 ---- #include "access/brin_xlog.h" #include "access/clog.h" #include "access/commit_ts.h" + #include "access/generic_xlog.h" #include "access/gin.h" #include "access/gist_private.h" #include "access/hash.h" diff --git a/src/include/access/generic_xlog.h b/src/include/access/generic_xlog.h new file mode 100644 index ...49249e0 *** a/src/include/access/generic_xlog.h --- b/src/include/access/generic_xlog.h *************** *** 0 **** --- 1,36 ---- + /*------------------------------------------------------------------------- + * + * generic_xlog.h + * Generic xlog API definition. + * + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/generic_xlog.h + * + *------------------------------------------------------------------------- + */ + #ifndef GENERIC_XLOG_H + #define GENERIC_XLOG_H + + #include "access/xlog.h" + #include "access/xlog_internal.h" + #include "storage/bufpage.h" + #include "utils/rel.h" + + #define MAX_GENERIC_XLOG_PAGES 3 + + /* API for construction of generic xlog records */ + extern void GenericXLogStart(Relation relation); + extern Page GenericXLogRegister(Buffer buffer, bool isNew); + extern void GenericXLogUnregister(Buffer buffer); + extern XLogRecPtr GenericXLogFinish(void); + extern void GenericXLogAbort(void); + + /* functions defined for rmgr */ + extern void generic_redo(XLogReaderState *record); + extern const char *generic_identify(uint8 info); + extern void generic_desc(StringInfo buf, XLogReaderState *record); + + #endif /* GENERIC_XLOG_H */ diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h new file mode 100644 index fab912d..3cfe6f7 *** a/src/include/access/rmgrlist.h --- b/src/include/access/rmgrlist.h *************** PG_RMGR(RM_SPGIST_ID, "SPGist", spg_redo *** 45,47 **** --- 45,48 ---- PG_RMGR(RM_BRIN_ID, "BRIN", brin_redo, brin_desc, brin_identify, NULL, NULL) PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_identify, NULL, NULL) PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL) + PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL)