Update the shell to use the recover extension for the .recover command.
FossilOrigin-Name: ae832e77084eddd696c80cb926d070a5db9d45dce34156a02522b3140e8f5e8b
This commit is contained in:
parent
3887ffe82a
commit
9a27d65044
@ -45,7 +45,6 @@ proc do_recover_test {tn} {
|
||||
uplevel [list do_test $tn.1 {
|
||||
set R [sqlite3_recover_init db main test.db2]
|
||||
$R config testdb rstate.db
|
||||
$R config rowids 1
|
||||
$R step
|
||||
$R finish
|
||||
} {}]
|
||||
@ -68,7 +67,6 @@ proc do_recover_test {tn} {
|
||||
|
||||
sqlite3 db2 test.db2
|
||||
execsql [join $::sqlhook ";"] db2
|
||||
# puts [join $::sqlhook ";\n"]
|
||||
uplevel [list do_test $tn.4 [list compare_dbs db db2] {}]
|
||||
db2 close
|
||||
}
|
||||
|
@ -84,6 +84,11 @@ struct sqlite3_recover {
|
||||
int (*xSql)(void*,const char*);
|
||||
};
|
||||
|
||||
/*
|
||||
** Default value for SQLITE_RECOVER_ROWIDS (sqlite3_recover.bRecoverRowid).
|
||||
*/
|
||||
#define RECOVER_ROWID_DEFAULT 1
|
||||
|
||||
/*
|
||||
** Like strlen(). But handles NULL pointer arguments.
|
||||
*/
|
||||
@ -371,7 +376,7 @@ static void recoverGetPage(
|
||||
** Try to use zA and zB first. If both of those are already found in z[]
|
||||
** then make up some string and store it in the buffer zBuf.
|
||||
*/
|
||||
static const char *unused_string(
|
||||
static const char *recoverUnusedString(
|
||||
const char *z, /* Result must not appear anywhere in z */
|
||||
const char *zA, const char *zB, /* Try these first */
|
||||
char *zBuf /* Space to store a generated string */
|
||||
@ -416,11 +421,11 @@ static void recoverEscapeCrnl(
|
||||
|
||||
for(i=0; zText[i]; i++){
|
||||
if( zNL==0 && zText[i]=='\n' ){
|
||||
zNL = unused_string(zText, "\\n", "\\012", zBuf1);
|
||||
zNL = recoverUnusedString(zText, "\\n", "\\012", zBuf1);
|
||||
nNL = (int)strlen(zNL);
|
||||
}
|
||||
if( zCR==0 && zText[i]=='\r' ){
|
||||
zCR = unused_string(zText, "\\r", "\\015", zBuf2);
|
||||
zCR = recoverUnusedString(zText, "\\r", "\\015", zBuf2);
|
||||
nCR = (int)strlen(zCR);
|
||||
}
|
||||
}
|
||||
@ -1281,6 +1286,7 @@ sqlite3_recover *recoverInit(
|
||||
memcpy(pRet->zUri, zUri, nUri);
|
||||
pRet->xSql = xSql;
|
||||
pRet->pSqlCtx = pSqlCtx;
|
||||
pRet->bRecoverRowid = RECOVER_ROWID_DEFAULT;
|
||||
}
|
||||
|
||||
return pRet;
|
||||
@ -1330,11 +1336,11 @@ int sqlite3_recover_config(sqlite3_recover *p, int op, void *pArg){
|
||||
break;
|
||||
|
||||
case SQLITE_RECOVER_FREELIST_CORRUPT:
|
||||
p->bFreelistCorrupt = (pArg ? 1 : 0);
|
||||
p->bFreelistCorrupt = *(int*)pArg;
|
||||
break;
|
||||
|
||||
case SQLITE_RECOVER_ROWIDS:
|
||||
p->bRecoverRowid = (pArg ? 1 : 0);
|
||||
p->bRecoverRowid = *(int*)pArg;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -16,7 +16,7 @@
|
||||
#ifndef _SQLITE_RECOVER_H
|
||||
#define _SQLITE_RECOVER_H
|
||||
|
||||
#include "sqlite3.h" /* Required for error code definitions */
|
||||
#include "sqlite3.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@ -58,9 +58,9 @@ int sqlite3_recover_config(sqlite3_recover*, int op, void *pArg);
|
||||
** pages to add to the lost-and-found table.
|
||||
**
|
||||
** SQLITE_RECOVER_FREELIST_CORRUPT:
|
||||
** The pArg value must actually be integer (type "int") value 0 or 1
|
||||
** cast as a (void*). If this option is set (argument is 1) and
|
||||
** a lost-and-found table has been configured using
|
||||
** The pArg value must actually be a pointer to a value of type
|
||||
** int containing value 0 or 1 cast as a (void*). If this option is set
|
||||
** (argument is 1) and a lost-and-found table has been configured using
|
||||
** SQLITE_RECOVER_LOST_AND_FOUND, then is assumed that the freelist is
|
||||
** corrupt and an attempt is made to recover records from pages that
|
||||
** appear to be linked into the freelist. Otherwise, pages on the freelist
|
||||
|
@ -129,7 +129,7 @@ static int testRecoverCmd(
|
||||
int iVal = 0;
|
||||
if( Tcl_GetBooleanFromObj(interp, objv[3], &iVal) ) return TCL_ERROR;
|
||||
res = sqlite3_recover_config(pTest->p,
|
||||
SQLITE_RECOVER_FREELIST_CORRUPT, SQLITE_INT_TO_PTR(iVal)
|
||||
SQLITE_RECOVER_FREELIST_CORRUPT, (void*)&iVal
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -137,7 +137,7 @@ static int testRecoverCmd(
|
||||
int iVal = 0;
|
||||
if( Tcl_GetBooleanFromObj(interp, objv[3], &iVal) ) return TCL_ERROR;
|
||||
res = sqlite3_recover_config(pTest->p,
|
||||
SQLITE_RECOVER_ROWIDS, SQLITE_INT_TO_PTR(iVal)
|
||||
SQLITE_RECOVER_ROWIDS, (void*)&iVal
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
20
manifest
20
manifest
@ -1,5 +1,5 @@
|
||||
C Fix\sproblems\swith\srecovering\sthe\ssqlite_sequence\stable.
|
||||
D 2022-09-08T17:42:33.679
|
||||
C Update\sthe\sshell\sto\suse\sthe\srecover\sextension\sfor\sthe\s.recover\scommand.
|
||||
D 2022-09-08T19:22:29.395
|
||||
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
||||
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
||||
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
|
||||
@ -387,14 +387,14 @@ F ext/rbu/rbuvacuum4.test a78898e438a44803eb2bc897ba3323373c9f277418e2d6d76e90f2
|
||||
F ext/rbu/sqlite3rbu.c 8737cabdfbee84bb25a7851ecef8b1312be332761238da9be6ddb10c62ad4291
|
||||
F ext/rbu/sqlite3rbu.h 1dc88ab7bd32d0f15890ea08d23476c4198d3da3056985403991f8c9cd389812
|
||||
F ext/rbu/test_rbu.c 03f6f177096a5f822d68d8e4069ad8907fe572c62ff2d19b141f59742821828a
|
||||
F ext/recover/recover1.test ddc322148170eafe1dabbea91ac175a72f7e7d2777619a6434696a310beff9a3
|
||||
F ext/recover/recover1.test aa9f7f46b7209cae6d52321052d4440bc8f82b93991e693c4bad20a6f05a53e5
|
||||
F ext/recover/recover_common.tcl 6679af7dffc858e345053a91c9b0a897595b4a13007aceffafca75304ccb137c
|
||||
F ext/recover/recoverclobber.test e6537ebf99f57bfff6cca59550b5f4278319b57a89865abb98d755a8fd561d84
|
||||
F ext/recover/recoverold.test f368a6ae2db12b6017257b332a19ab5df527f4061e43f12f5c85d8e2b236f074
|
||||
F ext/recover/recoverrowid.test ec4436cd69e6cdacb48dd2963ff6dd9dbd5fe648376de5e7c0c2f4f6cbacb417
|
||||
F ext/recover/sqlite3recover.c 2c45ab8cce41dcb578ef739652e65675d161751fe0d979b806d947a02de7fd32
|
||||
F ext/recover/sqlite3recover.h 32f89b66f0235c0d94dfee0f1c3e9ed1ad726b3c4f3716ef0262b31cc33e8301
|
||||
F ext/recover/test_recover.c 7aa268d3431d630eaa82ce14974ae04be50fe7feba660ffaea009cd581916d27
|
||||
F ext/recover/sqlite3recover.c b44241ca3b0cca2b50cfa7f715f79f4eddbe87d56e3baadd4e7bafe4c5872550
|
||||
F ext/recover/sqlite3recover.h 218cd9ba8c5c66c3841ca5014910982dc956cba5274257a0ecefb889db879133
|
||||
F ext/recover/test_recover.c 68b095ad396d8b1d9242ea663a4be1ad7585a46b1fc03483e9a692c8a87d2674
|
||||
F ext/repair/README.md 92f5e8aae749a4dae14f02eea8e1bb42d4db2b6ce5e83dbcdd6b1446997e0c15
|
||||
F ext/repair/checkfreelist.c e21f06995ff4efdc1622dcceaea4dcba2caa83ca2f31a1607b98a8509168a996
|
||||
F ext/repair/checkindex.c 4383e4469c21e5b9ae321d0d63cec53e981af9d7a6564be6374f0eeb93dfc890
|
||||
@ -597,7 +597,7 @@ F src/random.c 546d6feb15ec69c1aafe9bb351a277cbb498fd5410e646add673acb805714960
|
||||
F src/resolve.c efea4e5fbecfd6d0a9071b0be0d952620991673391b6ffaaf4c277b0bb674633
|
||||
F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92
|
||||
F src/select.c ccce37e7fbe71089cf6aec91e7134c9c0c1d4840cff9f02587bbc71240d914a5
|
||||
F src/shell.c.in e7e7c2c69ae86c5ee9e8ad66227203d46ff6dce8700a1b1dababff01c71d33df
|
||||
F src/shell.c.in b36581c005ebaa59b1eeb143bd3ad5a4b273bf15380ccac63ac8cdf4f0c4d3c9
|
||||
F src/sqlite.h.in b9b7fd73239d94db20332bb6e504688001e5564b655e1318a4427a1caef4b99e
|
||||
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
|
||||
F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d
|
||||
@ -2007,8 +2007,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
|
||||
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
||||
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
||||
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
||||
P cb4e950c472bd24a79a8505a7f8e4c3a0f7821648297d05cc760738b777d5149
|
||||
R e45081ba4805e1d831caf7a65d0c00b9
|
||||
P 356d2209ea5f6b69ce15b62027c63419c2d039e52f01c74a3810a6317abf4fb0
|
||||
R 9479c02b6838b39b62d6729211ce276e
|
||||
U dan
|
||||
Z 8c44bc5cd3a46bb8aa19fae7f571d64e
|
||||
Z df9c400431a19b9f6f7750b759f567b5
|
||||
# Remove this line to create a well-formed Fossil manifest.
|
||||
|
@ -1 +1 @@
|
||||
356d2209ea5f6b69ce15b62027c63419c2d039e52f01c74a3810a6317abf4fb0
|
||||
ae832e77084eddd696c80cb926d070a5db9d45dce34156a02522b3140e8f5e8b
|
658
src/shell.c.in
658
src/shell.c.in
@ -1039,6 +1039,8 @@ INCLUDE ../ext/expert/sqlite3expert.c
|
||||
|
||||
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||
INCLUDE ../ext/misc/dbdata.c
|
||||
INCLUDE ../ext/recover/sqlite3recover.h
|
||||
INCLUDE ../ext/recover/sqlite3recover.c
|
||||
#endif
|
||||
|
||||
#if defined(SQLITE_ENABLE_SESSION)
|
||||
@ -7252,363 +7254,15 @@ end_ar_command:
|
||||
#endif /* !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) */
|
||||
|
||||
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||
/*
|
||||
** If (*pRc) is not SQLITE_OK when this function is called, it is a no-op.
|
||||
** Otherwise, the SQL statement or statements in zSql are executed using
|
||||
** database connection db and the error code written to *pRc before
|
||||
** this function returns.
|
||||
*/
|
||||
static void shellExec(sqlite3 *db, int *pRc, const char *zSql){
|
||||
int rc = *pRc;
|
||||
if( rc==SQLITE_OK ){
|
||||
char *zErr = 0;
|
||||
rc = sqlite3_exec(db, zSql, 0, 0, &zErr);
|
||||
if( rc!=SQLITE_OK ){
|
||||
raw_printf(stderr, "SQL error: %s\n", zErr);
|
||||
}
|
||||
sqlite3_free(zErr);
|
||||
*pRc = rc;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Like shellExec(), except that zFmt is a printf() style format string.
|
||||
** This function is used as a callback by the recover extension. Simply
|
||||
** print the supplied SQL statement to stdout.
|
||||
*/
|
||||
static void shellExecPrintf(sqlite3 *db, int *pRc, const char *zFmt, ...){
|
||||
char *z = 0;
|
||||
if( *pRc==SQLITE_OK ){
|
||||
va_list ap;
|
||||
va_start(ap, zFmt);
|
||||
z = sqlite3_vmprintf(zFmt, ap);
|
||||
va_end(ap);
|
||||
if( z==0 ){
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}else{
|
||||
shellExec(db, pRc, z);
|
||||
}
|
||||
sqlite3_free(z);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** If *pRc is not SQLITE_OK when this function is called, it is a no-op.
|
||||
** Otherwise, an attempt is made to allocate, zero and return a pointer
|
||||
** to a buffer nByte bytes in size. If an OOM error occurs, *pRc is set
|
||||
** to SQLITE_NOMEM and NULL returned.
|
||||
*/
|
||||
static void *shellMalloc(int *pRc, sqlite3_int64 nByte){
|
||||
void *pRet = 0;
|
||||
if( *pRc==SQLITE_OK ){
|
||||
pRet = sqlite3_malloc64(nByte);
|
||||
if( pRet==0 ){
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}else{
|
||||
memset(pRet, 0, nByte);
|
||||
}
|
||||
}
|
||||
return pRet;
|
||||
}
|
||||
|
||||
/*
|
||||
** If *pRc is not SQLITE_OK when this function is called, it is a no-op.
|
||||
** Otherwise, zFmt is treated as a printf() style string. The result of
|
||||
** formatting it along with any trailing arguments is written into a
|
||||
** buffer obtained from sqlite3_malloc(), and pointer to which is returned.
|
||||
** It is the responsibility of the caller to eventually free this buffer
|
||||
** using a call to sqlite3_free().
|
||||
**
|
||||
** If an OOM error occurs, (*pRc) is set to SQLITE_NOMEM and a NULL
|
||||
** pointer returned.
|
||||
*/
|
||||
static char *shellMPrintf(int *pRc, const char *zFmt, ...){
|
||||
char *z = 0;
|
||||
if( *pRc==SQLITE_OK ){
|
||||
va_list ap;
|
||||
va_start(ap, zFmt);
|
||||
z = sqlite3_vmprintf(zFmt, ap);
|
||||
va_end(ap);
|
||||
if( z==0 ){
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** When running the ".recover" command, each output table, and the special
|
||||
** orphaned row table if it is required, is represented by an instance
|
||||
** of the following struct.
|
||||
*/
|
||||
typedef struct RecoverTable RecoverTable;
|
||||
struct RecoverTable {
|
||||
char *zQuoted; /* Quoted version of table name */
|
||||
int nCol; /* Number of columns in table */
|
||||
char **azlCol; /* Array of column lists */
|
||||
int iPk; /* Index of IPK column */
|
||||
};
|
||||
|
||||
/*
|
||||
** Free a RecoverTable object allocated by recoverFindTable() or
|
||||
** recoverOrphanTable().
|
||||
*/
|
||||
static void recoverFreeTable(RecoverTable *pTab){
|
||||
if( pTab ){
|
||||
sqlite3_free(pTab->zQuoted);
|
||||
if( pTab->azlCol ){
|
||||
int i;
|
||||
for(i=0; i<=pTab->nCol; i++){
|
||||
sqlite3_free(pTab->azlCol[i]);
|
||||
}
|
||||
sqlite3_free(pTab->azlCol);
|
||||
}
|
||||
sqlite3_free(pTab);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** This function is a no-op if (*pRc) is not SQLITE_OK when it is called.
|
||||
** Otherwise, it allocates and returns a RecoverTable object based on the
|
||||
** final four arguments passed to this function. It is the responsibility
|
||||
** of the caller to eventually free the returned object using
|
||||
** recoverFreeTable().
|
||||
*/
|
||||
static RecoverTable *recoverNewTable(
|
||||
int *pRc, /* IN/OUT: Error code */
|
||||
const char *zName, /* Name of table */
|
||||
const char *zSql, /* CREATE TABLE statement */
|
||||
int bIntkey,
|
||||
int nCol
|
||||
){
|
||||
sqlite3 *dbtmp = 0; /* sqlite3 handle for testing CREATE TABLE */
|
||||
int rc = *pRc;
|
||||
RecoverTable *pTab = 0;
|
||||
|
||||
pTab = (RecoverTable*)shellMalloc(&rc, sizeof(RecoverTable));
|
||||
if( rc==SQLITE_OK ){
|
||||
int nSqlCol = 0;
|
||||
int bSqlIntkey = 0;
|
||||
sqlite3_stmt *pStmt = 0;
|
||||
|
||||
rc = sqlite3_open("", &dbtmp);
|
||||
if( rc==SQLITE_OK ){
|
||||
sqlite3_create_function(dbtmp, "shell_idquote", 1, SQLITE_UTF8, 0,
|
||||
shellIdQuote, 0, 0);
|
||||
}
|
||||
if( rc==SQLITE_OK ){
|
||||
rc = sqlite3_exec(dbtmp, "PRAGMA writable_schema = on", 0, 0, 0);
|
||||
}
|
||||
if( rc==SQLITE_OK ){
|
||||
rc = sqlite3_exec(dbtmp, zSql, 0, 0, 0);
|
||||
if( rc==SQLITE_ERROR ){
|
||||
rc = SQLITE_OK;
|
||||
goto finished;
|
||||
}
|
||||
}
|
||||
shellPreparePrintf(dbtmp, &rc, &pStmt,
|
||||
"SELECT count(*) FROM pragma_table_info(%Q)", zName
|
||||
);
|
||||
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
nSqlCol = sqlite3_column_int(pStmt, 0);
|
||||
}
|
||||
shellFinalize(&rc, pStmt);
|
||||
|
||||
if( rc!=SQLITE_OK || nSqlCol<nCol ){
|
||||
goto finished;
|
||||
}
|
||||
|
||||
shellPreparePrintf(dbtmp, &rc, &pStmt,
|
||||
"SELECT ("
|
||||
" SELECT substr(data,1,1)==X'0D' FROM sqlite_dbpage WHERE pgno=rootpage"
|
||||
") FROM sqlite_schema WHERE name = %Q", zName
|
||||
);
|
||||
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
bSqlIntkey = sqlite3_column_int(pStmt, 0);
|
||||
}
|
||||
shellFinalize(&rc, pStmt);
|
||||
|
||||
if( bIntkey==bSqlIntkey ){
|
||||
int i;
|
||||
const char *zPk = "_rowid_";
|
||||
sqlite3_stmt *pPkFinder = 0;
|
||||
|
||||
/* If this is an intkey table and there is an INTEGER PRIMARY KEY,
|
||||
** set zPk to the name of the PK column, and pTab->iPk to the index
|
||||
** of the column, where columns are 0-numbered from left to right.
|
||||
** Or, if this is a WITHOUT ROWID table or if there is no IPK column,
|
||||
** leave zPk as "_rowid_" and pTab->iPk at -2. */
|
||||
pTab->iPk = -2;
|
||||
if( bIntkey ){
|
||||
shellPreparePrintf(dbtmp, &rc, &pPkFinder,
|
||||
"SELECT cid, name FROM pragma_table_info(%Q) "
|
||||
" WHERE pk=1 AND type='integer' COLLATE nocase"
|
||||
" AND NOT EXISTS (SELECT cid FROM pragma_table_info(%Q) WHERE pk=2)"
|
||||
, zName, zName
|
||||
);
|
||||
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPkFinder) ){
|
||||
pTab->iPk = sqlite3_column_int(pPkFinder, 0);
|
||||
zPk = (const char*)sqlite3_column_text(pPkFinder, 1);
|
||||
if( zPk==0 ){ zPk = "_"; /* Defensive. Should never happen */ }
|
||||
}
|
||||
}
|
||||
|
||||
pTab->zQuoted = shellMPrintf(&rc, "\"%w\"", zName);
|
||||
pTab->azlCol = (char**)shellMalloc(&rc, sizeof(char*) * (nSqlCol+1));
|
||||
pTab->nCol = nSqlCol;
|
||||
|
||||
if( bIntkey ){
|
||||
pTab->azlCol[0] = shellMPrintf(&rc, "\"%w\"", zPk);
|
||||
}else{
|
||||
pTab->azlCol[0] = shellMPrintf(&rc, "");
|
||||
}
|
||||
i = 1;
|
||||
shellPreparePrintf(dbtmp, &rc, &pStmt,
|
||||
"SELECT %Q || group_concat(shell_idquote(name), ', ') "
|
||||
" FILTER (WHERE cid!=%d) OVER (ORDER BY %s cid) "
|
||||
"FROM pragma_table_info(%Q)",
|
||||
bIntkey ? ", " : "", pTab->iPk,
|
||||
bIntkey ? "" : "(CASE WHEN pk=0 THEN 1000000 ELSE pk END), ",
|
||||
zName
|
||||
);
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
const char *zText = (const char*)sqlite3_column_text(pStmt, 0);
|
||||
pTab->azlCol[i] = shellMPrintf(&rc, "%s%s", pTab->azlCol[0], zText);
|
||||
i++;
|
||||
}
|
||||
shellFinalize(&rc, pStmt);
|
||||
|
||||
shellFinalize(&rc, pPkFinder);
|
||||
}
|
||||
}
|
||||
|
||||
finished:
|
||||
sqlite3_close(dbtmp);
|
||||
*pRc = rc;
|
||||
if( rc!=SQLITE_OK || (pTab && pTab->zQuoted==0) ){
|
||||
recoverFreeTable(pTab);
|
||||
pTab = 0;
|
||||
}
|
||||
return pTab;
|
||||
}
|
||||
|
||||
/*
|
||||
** This function is called to search the schema recovered from the
|
||||
** sqlite_schema table of the (possibly) corrupt database as part
|
||||
** of a ".recover" command. Specifically, for a table with root page
|
||||
** iRoot and at least nCol columns. Additionally, if bIntkey is 0, the
|
||||
** table must be a WITHOUT ROWID table, or if non-zero, not one of
|
||||
** those.
|
||||
**
|
||||
** If a table is found, a (RecoverTable*) object is returned. Or, if
|
||||
** no such table is found, but bIntkey is false and iRoot is the
|
||||
** root page of an index in the recovered schema, then (*pbNoop) is
|
||||
** set to true and NULL returned. Or, if there is no such table or
|
||||
** index, NULL is returned and (*pbNoop) set to 0, indicating that
|
||||
** the caller should write data to the orphans table.
|
||||
*/
|
||||
static RecoverTable *recoverFindTable(
|
||||
ShellState *pState, /* Shell state object */
|
||||
int *pRc, /* IN/OUT: Error code */
|
||||
int iRoot, /* Root page of table */
|
||||
int bIntkey, /* True for an intkey table */
|
||||
int nCol, /* Number of columns in table */
|
||||
int *pbNoop /* OUT: True if iRoot is root of index */
|
||||
){
|
||||
sqlite3_stmt *pStmt = 0;
|
||||
RecoverTable *pRet = 0;
|
||||
int bNoop = 0;
|
||||
const char *zSql = 0;
|
||||
const char *zName = 0;
|
||||
|
||||
/* Search the recovered schema for an object with root page iRoot. */
|
||||
shellPreparePrintf(pState->db, pRc, &pStmt,
|
||||
"SELECT type, name, sql FROM recovery.schema WHERE rootpage=%d", iRoot
|
||||
);
|
||||
while( *pRc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
const char *zType = (const char*)sqlite3_column_text(pStmt, 0);
|
||||
if( bIntkey==0 && sqlite3_stricmp(zType, "index")==0 ){
|
||||
bNoop = 1;
|
||||
break;
|
||||
}
|
||||
if( sqlite3_stricmp(zType, "table")==0 ){
|
||||
zName = (const char*)sqlite3_column_text(pStmt, 1);
|
||||
zSql = (const char*)sqlite3_column_text(pStmt, 2);
|
||||
if( zName!=0 && zSql!=0 ){
|
||||
pRet = recoverNewTable(pRc, zName, zSql, bIntkey, nCol);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shellFinalize(pRc, pStmt);
|
||||
*pbNoop = bNoop;
|
||||
return pRet;
|
||||
}
|
||||
|
||||
/*
|
||||
** Return a RecoverTable object representing the orphans table.
|
||||
*/
|
||||
static RecoverTable *recoverOrphanTable(
|
||||
ShellState *pState, /* Shell state object */
|
||||
int *pRc, /* IN/OUT: Error code */
|
||||
const char *zLostAndFound, /* Base name for orphans table */
|
||||
int nCol /* Number of user data columns */
|
||||
){
|
||||
RecoverTable *pTab = 0;
|
||||
if( nCol>=0 && *pRc==SQLITE_OK ){
|
||||
int i;
|
||||
|
||||
/* This block determines the name of the orphan table. The prefered
|
||||
** name is zLostAndFound. But if that clashes with another name
|
||||
** in the recovered schema, try zLostAndFound_0, zLostAndFound_1
|
||||
** and so on until a non-clashing name is found. */
|
||||
int iTab = 0;
|
||||
char *zTab = shellMPrintf(pRc, "%s", zLostAndFound);
|
||||
sqlite3_stmt *pTest = 0;
|
||||
shellPrepare(pState->db, pRc,
|
||||
"SELECT 1 FROM recovery.schema WHERE name=?", &pTest
|
||||
);
|
||||
if( pTest ) sqlite3_bind_text(pTest, 1, zTab, -1, SQLITE_TRANSIENT);
|
||||
while( *pRc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pTest) ){
|
||||
shellReset(pRc, pTest);
|
||||
sqlite3_free(zTab);
|
||||
zTab = shellMPrintf(pRc, "%s_%d", zLostAndFound, iTab++);
|
||||
sqlite3_bind_text(pTest, 1, zTab, -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
shellFinalize(pRc, pTest);
|
||||
|
||||
pTab = (RecoverTable*)shellMalloc(pRc, sizeof(RecoverTable));
|
||||
if( pTab ){
|
||||
pTab->zQuoted = shellMPrintf(pRc, "\"%w\"", zTab);
|
||||
pTab->nCol = nCol;
|
||||
pTab->iPk = -2;
|
||||
if( nCol>0 ){
|
||||
pTab->azlCol = (char**)shellMalloc(pRc, sizeof(char*) * (nCol+1));
|
||||
if( pTab->azlCol ){
|
||||
pTab->azlCol[nCol] = shellMPrintf(pRc, "");
|
||||
for(i=nCol-1; i>=0; i--){
|
||||
pTab->azlCol[i] = shellMPrintf(pRc, "%s, NULL", pTab->azlCol[i+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( *pRc!=SQLITE_OK ){
|
||||
recoverFreeTable(pTab);
|
||||
pTab = 0;
|
||||
}else{
|
||||
raw_printf(pState->out,
|
||||
"CREATE TABLE %s(rootpgno INTEGER, "
|
||||
"pgno INTEGER, nfield INTEGER, id INTEGER", pTab->zQuoted
|
||||
);
|
||||
for(i=0; i<nCol; i++){
|
||||
raw_printf(pState->out, ", c%d", i);
|
||||
}
|
||||
raw_printf(pState->out, ");\n");
|
||||
}
|
||||
}
|
||||
sqlite3_free(zTab);
|
||||
}
|
||||
return pTab;
|
||||
static int recoverSqlCb(void *pCtx, const char *zSql){
|
||||
ShellState *pState = (ShellState*)pCtx;
|
||||
raw_printf(stdout, "%s;\n", zSql);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -7618,17 +7272,13 @@ static RecoverTable *recoverOrphanTable(
|
||||
*/
|
||||
static int recoverDatabaseCmd(ShellState *pState, int nArg, char **azArg){
|
||||
int rc = SQLITE_OK;
|
||||
sqlite3_stmt *pLoop = 0; /* Loop through all root pages */
|
||||
sqlite3_stmt *pPages = 0; /* Loop through all pages in a group */
|
||||
sqlite3_stmt *pCells = 0; /* Loop through all cells in a page */
|
||||
const char *zRecoveryDb = ""; /* Name of "recovery" database */
|
||||
const char *zLostAndFound = "lost_and_found";
|
||||
int i;
|
||||
int nOrphan = -1;
|
||||
RecoverTable *pOrphan = 0;
|
||||
|
||||
const char *zLAF = "lost_and_found";
|
||||
int bFreelist = 1; /* 0 if --freelist-corrupt is specified */
|
||||
int bRowids = 1; /* 0 if --no-rowids */
|
||||
sqlite3_recover *p = 0;
|
||||
int i = 0;
|
||||
|
||||
for(i=1; i<nArg; i++){
|
||||
char *z = azArg[i];
|
||||
int n;
|
||||
@ -7643,7 +7293,7 @@ static int recoverDatabaseCmd(ShellState *pState, int nArg, char **azArg){
|
||||
}else
|
||||
if( n<=15 && memcmp("-lost-and-found", z, n)==0 && i<(nArg-1) ){
|
||||
i++;
|
||||
zLostAndFound = azArg[i];
|
||||
zLAF = azArg[i];
|
||||
}else
|
||||
if( n<=10 && memcmp("-no-rowids", z, n)==0 ){
|
||||
bRowids = 0;
|
||||
@ -7655,278 +7305,22 @@ static int recoverDatabaseCmd(ShellState *pState, int nArg, char **azArg){
|
||||
}
|
||||
}
|
||||
|
||||
shellExecPrintf(pState->db, &rc,
|
||||
/* Attach an in-memory database named 'recovery'. Create an indexed
|
||||
** cache of the sqlite_dbptr virtual table. */
|
||||
"PRAGMA writable_schema = on;"
|
||||
"ATTACH %Q AS recovery;"
|
||||
"DROP TABLE IF EXISTS recovery.dbptr;"
|
||||
"DROP TABLE IF EXISTS recovery.freelist;"
|
||||
"DROP TABLE IF EXISTS recovery.map;"
|
||||
"DROP TABLE IF EXISTS recovery.schema;"
|
||||
"CREATE TABLE recovery.freelist(pgno INTEGER PRIMARY KEY);", zRecoveryDb
|
||||
p = sqlite3_recover_init_sql(
|
||||
pState->db, "main", recoverSqlCb, (void*)pState
|
||||
);
|
||||
|
||||
if( bFreelist ){
|
||||
shellExec(pState->db, &rc,
|
||||
"WITH trunk(pgno) AS ("
|
||||
" SELECT shell_int32("
|
||||
" (SELECT data FROM sqlite_dbpage WHERE pgno=1), 8) AS x "
|
||||
" WHERE x>0"
|
||||
" UNION"
|
||||
" SELECT shell_int32("
|
||||
" (SELECT data FROM sqlite_dbpage WHERE pgno=trunk.pgno), 0) AS x "
|
||||
" FROM trunk WHERE x>0"
|
||||
"),"
|
||||
"freelist(data, n, freepgno) AS ("
|
||||
" SELECT data, min(16384, shell_int32(data, 1)-1), t.pgno "
|
||||
" FROM trunk t, sqlite_dbpage s WHERE s.pgno=t.pgno"
|
||||
" UNION ALL"
|
||||
" SELECT data, n-1, shell_int32(data, 2+n) "
|
||||
" FROM freelist WHERE n>=0"
|
||||
")"
|
||||
"REPLACE INTO recovery.freelist SELECT freepgno FROM freelist;"
|
||||
);
|
||||
sqlite3_recover_config(p, SQLITE_RECOVER_TESTDB, (void*)zRecoveryDb);
|
||||
sqlite3_recover_config(p, SQLITE_RECOVER_LOST_AND_FOUND, (void*)zLAF);
|
||||
sqlite3_recover_config(p, SQLITE_RECOVER_ROWIDS, (void*)&bRowids);
|
||||
sqlite3_recover_config(p, SQLITE_RECOVER_FREELIST_CORRUPT,(void*)&bFreelist);
|
||||
|
||||
sqlite3_recover_step(p);
|
||||
if( sqlite3_recover_errcode(p)!=SQLITE_OK ){
|
||||
const char *zErr = sqlite3_recover_errmsg(p);
|
||||
int errCode = sqlite3_recover_errcode(p);
|
||||
raw_printf(stderr, "sql error: %s (%d)\n", zErr, errCode);
|
||||
}
|
||||
|
||||
/* If this is an auto-vacuum database, add all pointer-map pages to
|
||||
** the freelist table. Do this regardless of whether or not
|
||||
** --freelist-corrupt was specified. */
|
||||
shellExec(pState->db, &rc,
|
||||
"WITH ptrmap(pgno) AS ("
|
||||
" SELECT 2 WHERE shell_int32("
|
||||
" (SELECT data FROM sqlite_dbpage WHERE pgno=1), 13"
|
||||
" )"
|
||||
" UNION ALL "
|
||||
" SELECT pgno+1+(SELECT page_size FROM pragma_page_size)/5 AS pp "
|
||||
" FROM ptrmap WHERE pp<=(SELECT page_count FROM pragma_page_count)"
|
||||
")"
|
||||
"REPLACE INTO recovery.freelist SELECT pgno FROM ptrmap"
|
||||
);
|
||||
|
||||
shellExec(pState->db, &rc,
|
||||
"CREATE TABLE recovery.dbptr("
|
||||
" pgno, child, PRIMARY KEY(child, pgno)"
|
||||
") WITHOUT ROWID;"
|
||||
"INSERT OR IGNORE INTO recovery.dbptr(pgno, child) "
|
||||
" SELECT * FROM sqlite_dbptr"
|
||||
" WHERE pgno NOT IN freelist AND child NOT IN freelist;"
|
||||
|
||||
/* Delete any pointer to page 1. This ensures that page 1 is considered
|
||||
** a root page, regardless of how corrupt the db is. */
|
||||
"DELETE FROM recovery.dbptr WHERE child = 1;"
|
||||
|
||||
/* Delete all pointers to any pages that have more than one pointer
|
||||
** to them. Such pages will be treated as root pages when recovering
|
||||
** data. */
|
||||
"DELETE FROM recovery.dbptr WHERE child IN ("
|
||||
" SELECT child FROM recovery.dbptr GROUP BY child HAVING count(*)>1"
|
||||
");"
|
||||
|
||||
/* Create the "map" table that will (eventually) contain instructions
|
||||
** for dealing with each page in the db that contains one or more
|
||||
** records. */
|
||||
"CREATE TABLE recovery.map("
|
||||
"pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT"
|
||||
");"
|
||||
|
||||
/* Populate table [map]. If there are circular loops of pages in the
|
||||
** database, the following adds all pages in such a loop to the map
|
||||
** as individual root pages. This could be handled better. */
|
||||
"WITH pages(i, maxlen) AS ("
|
||||
" SELECT page_count, ("
|
||||
" SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=page_count"
|
||||
" ) FROM pragma_page_count WHERE page_count>0"
|
||||
" UNION ALL"
|
||||
" SELECT i-1, ("
|
||||
" SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=i-1"
|
||||
" ) FROM pages WHERE i>=2"
|
||||
")"
|
||||
"INSERT INTO recovery.map(pgno, maxlen, intkey, root) "
|
||||
" SELECT i, maxlen, NULL, ("
|
||||
" WITH p(orig, pgno, parent) AS ("
|
||||
" SELECT 0, i, (SELECT pgno FROM recovery.dbptr WHERE child=i)"
|
||||
" UNION "
|
||||
" SELECT i, p.parent, "
|
||||
" (SELECT pgno FROM recovery.dbptr WHERE child=p.parent) FROM p"
|
||||
" )"
|
||||
" SELECT pgno FROM p WHERE (parent IS NULL OR pgno = orig)"
|
||||
") "
|
||||
"FROM pages WHERE maxlen IS NOT NULL AND i NOT IN freelist;"
|
||||
"UPDATE recovery.map AS o SET intkey = ("
|
||||
" SELECT substr(data, 1, 1)==X'0D' FROM sqlite_dbpage WHERE pgno=o.pgno"
|
||||
");"
|
||||
|
||||
/* Extract data from page 1 and any linked pages into table
|
||||
** recovery.schema. With the same schema as an sqlite_schema table. */
|
||||
"CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);"
|
||||
"INSERT INTO recovery.schema SELECT "
|
||||
" max(CASE WHEN field=0 THEN value ELSE NULL END),"
|
||||
" max(CASE WHEN field=1 THEN value ELSE NULL END),"
|
||||
" max(CASE WHEN field=2 THEN value ELSE NULL END),"
|
||||
" max(CASE WHEN field=3 THEN value ELSE NULL END),"
|
||||
" max(CASE WHEN field=4 THEN value ELSE NULL END)"
|
||||
"FROM sqlite_dbdata WHERE pgno IN ("
|
||||
" SELECT pgno FROM recovery.map WHERE root=1"
|
||||
")"
|
||||
"GROUP BY pgno, cell;"
|
||||
"CREATE INDEX recovery.schema_rootpage ON schema(rootpage);"
|
||||
);
|
||||
|
||||
/* Open a transaction, then print out all non-virtual, non-"sqlite_%"
|
||||
** CREATE TABLE statements that extracted from the existing schema. */
|
||||
if( rc==SQLITE_OK ){
|
||||
sqlite3_stmt *pStmt = 0;
|
||||
/* ".recover" might output content in an order which causes immediate
|
||||
** foreign key constraints to be violated. So disable foreign-key
|
||||
** constraint enforcement to prevent problems when running the output
|
||||
** script. */
|
||||
raw_printf(pState->out, "PRAGMA foreign_keys=OFF;\n");
|
||||
raw_printf(pState->out, "BEGIN;\n");
|
||||
raw_printf(pState->out, "PRAGMA writable_schema = on;\n");
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT sql FROM recovery.schema "
|
||||
"WHERE type='table' AND sql LIKE 'create table%'", &pStmt
|
||||
);
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
const char *zCreateTable = (const char*)sqlite3_column_text(pStmt, 0);
|
||||
raw_printf(pState->out, "CREATE TABLE IF NOT EXISTS %s;\n",
|
||||
&zCreateTable[12]
|
||||
);
|
||||
}
|
||||
shellFinalize(&rc, pStmt);
|
||||
}
|
||||
|
||||
/* Figure out if an orphan table will be required. And if so, how many
|
||||
** user columns it should contain */
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT coalesce(max(maxlen), -2) FROM recovery.map WHERE root>1"
|
||||
, &pLoop
|
||||
);
|
||||
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
|
||||
nOrphan = sqlite3_column_int(pLoop, 0);
|
||||
}
|
||||
shellFinalize(&rc, pLoop);
|
||||
pLoop = 0;
|
||||
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT pgno FROM recovery.map WHERE root=?", &pPages
|
||||
);
|
||||
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT max(field), group_concat(shell_escape_crnl(quote"
|
||||
"(case when (? AND field<0) then NULL else value end)"
|
||||
"), ', ')"
|
||||
", min(field) "
|
||||
"FROM sqlite_dbdata WHERE pgno = ? AND field != ?"
|
||||
"GROUP BY cell", &pCells
|
||||
);
|
||||
|
||||
/* Loop through each root page. */
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT root, intkey, max(maxlen) FROM recovery.map"
|
||||
" WHERE root>1 GROUP BY root, intkey ORDER BY root=("
|
||||
" SELECT rootpage FROM recovery.schema WHERE name='sqlite_sequence'"
|
||||
")", &pLoop
|
||||
);
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
|
||||
int iRoot = sqlite3_column_int(pLoop, 0);
|
||||
int bIntkey = sqlite3_column_int(pLoop, 1);
|
||||
int nCol = sqlite3_column_int(pLoop, 2);
|
||||
int bNoop = 0;
|
||||
RecoverTable *pTab;
|
||||
|
||||
assert( bIntkey==0 || bIntkey==1 );
|
||||
pTab = recoverFindTable(pState, &rc, iRoot, bIntkey, nCol, &bNoop);
|
||||
if( bNoop || rc ) continue;
|
||||
if( pTab==0 ){
|
||||
if( pOrphan==0 ){
|
||||
pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan);
|
||||
}
|
||||
pTab = pOrphan;
|
||||
if( pTab==0 ) break;
|
||||
}
|
||||
|
||||
if( 0==sqlite3_stricmp(pTab->zQuoted, "\"sqlite_sequence\"") ){
|
||||
raw_printf(pState->out, "DELETE FROM sqlite_sequence;\n");
|
||||
}
|
||||
sqlite3_bind_int(pPages, 1, iRoot);
|
||||
if( bRowids==0 && pTab->iPk<0 ){
|
||||
sqlite3_bind_int(pCells, 1, 1);
|
||||
}else{
|
||||
sqlite3_bind_int(pCells, 1, 0);
|
||||
}
|
||||
sqlite3_bind_int(pCells, 3, pTab->iPk);
|
||||
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPages) ){
|
||||
int iPgno = sqlite3_column_int(pPages, 0);
|
||||
sqlite3_bind_int(pCells, 2, iPgno);
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pCells) ){
|
||||
int nField = sqlite3_column_int(pCells, 0);
|
||||
int iMin = sqlite3_column_int(pCells, 2);
|
||||
const char *zVal = (const char*)sqlite3_column_text(pCells, 1);
|
||||
|
||||
RecoverTable *pTab2 = pTab;
|
||||
if( pTab!=pOrphan && (iMin<0)!=bIntkey ){
|
||||
if( pOrphan==0 ){
|
||||
pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan);
|
||||
}
|
||||
pTab2 = pOrphan;
|
||||
if( pTab2==0 ) break;
|
||||
}
|
||||
|
||||
nField = nField+1;
|
||||
if( pTab2==pOrphan ){
|
||||
raw_printf(pState->out,
|
||||
"INSERT INTO %s VALUES(%d, %d, %d, %s%s%s);\n",
|
||||
pTab2->zQuoted, iRoot, iPgno, nField,
|
||||
iMin<0 ? "" : "NULL, ", zVal, pTab2->azlCol[nField]
|
||||
);
|
||||
}else{
|
||||
raw_printf(pState->out, "INSERT INTO %s(%s) VALUES( %s );\n",
|
||||
pTab2->zQuoted, pTab2->azlCol[nField], zVal
|
||||
);
|
||||
}
|
||||
}
|
||||
shellReset(&rc, pCells);
|
||||
}
|
||||
shellReset(&rc, pPages);
|
||||
if( pTab!=pOrphan ) recoverFreeTable(pTab);
|
||||
}
|
||||
shellFinalize(&rc, pLoop);
|
||||
shellFinalize(&rc, pPages);
|
||||
shellFinalize(&rc, pCells);
|
||||
recoverFreeTable(pOrphan);
|
||||
|
||||
/* The rest of the schema */
|
||||
if( rc==SQLITE_OK ){
|
||||
sqlite3_stmt *pStmt = 0;
|
||||
shellPrepare(pState->db, &rc,
|
||||
"SELECT sql, name FROM recovery.schema "
|
||||
"WHERE sql NOT LIKE 'create table%'", &pStmt
|
||||
);
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
const char *zSql = (const char*)sqlite3_column_text(pStmt, 0);
|
||||
if( sqlite3_strnicmp(zSql, "create virt", 11)==0 ){
|
||||
const char *zName = (const char*)sqlite3_column_text(pStmt, 1);
|
||||
char *zPrint = shellMPrintf(&rc,
|
||||
"INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)",
|
||||
zName, zName, zSql
|
||||
);
|
||||
raw_printf(pState->out, "%s;\n", zPrint);
|
||||
sqlite3_free(zPrint);
|
||||
}else{
|
||||
raw_printf(pState->out, "%s;\n", zSql);
|
||||
}
|
||||
}
|
||||
shellFinalize(&rc, pStmt);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
raw_printf(pState->out, "PRAGMA writable_schema = off;\n");
|
||||
raw_printf(pState->out, "COMMIT;\n");
|
||||
}
|
||||
sqlite3_exec(pState->db, "DETACH recovery", 0, 0, 0);
|
||||
rc = sqlite3_recover_finish(p);
|
||||
return rc;
|
||||
}
|
||||
#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
|
||||
|
Loading…
x
Reference in New Issue
Block a user