sqlite/ext/recover/sqlite3recover.c

729 lines
19 KiB
C
Raw Normal View History

/*
** 2022-08-27
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
**
*/
#include "sqlite3recover.h"
#include <assert.h>
#include <string.h>
typedef unsigned int u32;
typedef sqlite3_int64 i64;
typedef struct RecoverColumn RecoverColumn;
struct RecoverColumn {
char *zCol;
int eHidden;
};
#define RECOVER_EHIDDEN_NONE 0
#define RECOVER_EHIDDEN_HIDDEN 1
#define RECOVER_EHIDDEN_VIRTUAL 2
#define RECOVER_EHIDDEN_STORED 3
/*
** 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 {
u32 iRoot; /* Root page in original database */
char *zTab; /* Name of table */
int nCol; /* Number of columns in table */
RecoverColumn *aCol; /* Array of columns */
int bIntkey; /* True for intkey, false for without rowid */
int iPk; /* Index of IPK column, if bIntkey */
RecoverTable *pNext;
};
/*
**
*/
#define RECOVERY_SCHEMA \
" CREATE TABLE recovery.freelist(" \
" pgno INTEGER PRIMARY KEY" \
" );" \
" CREATE TABLE recovery.dbptr(" \
" pgno, child, PRIMARY KEY(child, pgno)" \
" ) WITHOUT ROWID;" \
" CREATE TABLE recovery.map(" \
" pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT" \
" );" \
" CREATE TABLE recovery.schema(" \
" type, name, tbl_name, rootpage, sql" \
" );"
struct sqlite3_recover {
sqlite3 *dbIn;
sqlite3 *dbOut;
sqlite3_stmt *pGetPage;
char *zDb;
char *zUri;
RecoverTable *pTblList;
int errCode; /* For sqlite3_recover_errcode() */
char *zErrMsg; /* For sqlite3_recover_errmsg() */
char *zStateDb;
};
/*
** Like strlen(). But handles NULL pointer arguments.
*/
static int recoverStrlen(const char *zStr){
int nRet = 0;
if( zStr ){
while( zStr[nRet] ) nRet++;
}
return nRet;
}
static void *recoverMalloc(sqlite3_recover *p, sqlite3_int64 nByte){
void *pRet = 0;
assert( nByte>0 );
if( p->errCode==SQLITE_OK ){
pRet = sqlite3_malloc64(nByte);
if( pRet ){
memset(pRet, 0, nByte);
}else{
p->errCode = SQLITE_NOMEM;
}
}
return pRet;
}
static int recoverError(
sqlite3_recover *p,
int errCode,
const char *zFmt, ...
){
va_list ap;
char *z;
va_start(ap, zFmt);
z = sqlite3_vmprintf(zFmt, ap);
va_end(ap);
sqlite3_free(p->zErrMsg);
p->zErrMsg = z;
p->errCode = errCode;
return errCode;
}
static int recoverDbError(sqlite3_recover *p, sqlite3 *db){
return recoverError(p, sqlite3_errcode(db), "%s", sqlite3_errmsg(db));
}
static sqlite3_stmt *recoverPrepare(
sqlite3_recover *p,
sqlite3 *db,
const char *zSql
){
sqlite3_stmt *pStmt = 0;
if( p->errCode==SQLITE_OK ){
if( sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0) ){
recoverDbError(p, db);
}
}
return pStmt;
}
/*
** Create a prepared statement using printf-style arguments for the SQL.
*/
static sqlite3_stmt *recoverPreparePrintf(
sqlite3_recover *p,
sqlite3 *db,
const char *zFmt, ...
){
sqlite3_stmt *pStmt = 0;
if( p->errCode==SQLITE_OK ){
va_list ap;
char *z;
va_start(ap, zFmt);
z = sqlite3_vmprintf(zFmt, ap);
va_end(ap);
if( z==0 ){
p->errCode = SQLITE_NOMEM;
}else{
pStmt = recoverPrepare(p, db, z);
sqlite3_free(z);
}
}
return pStmt;
}
static sqlite3_stmt *recoverReset(sqlite3_recover *p, sqlite3_stmt *pStmt){
int rc = sqlite3_reset(pStmt);
if( rc!=SQLITE_OK && p->errCode==SQLITE_OK ){
recoverDbError(p, sqlite3_db_handle(pStmt));
}
return pStmt;
}
static void recoverFinalize(sqlite3_recover *p, sqlite3_stmt *pStmt){
sqlite3 *db = sqlite3_db_handle(pStmt);
int rc = sqlite3_finalize(pStmt);
if( rc!=SQLITE_OK && p->errCode==SQLITE_OK ){
recoverDbError(p, db);
}
}
static int recoverExec(sqlite3_recover *p, sqlite3 *db, const char *zSql){
if( p->errCode==SQLITE_OK ){
int rc = sqlite3_exec(p->dbOut, zSql, 0, 0, 0);
if( rc ){
recoverDbError(p, p->dbOut);
}
}
return p->errCode;
}
/*
** The implementation of a user-defined SQL function invoked by the
** sqlite_dbdata and sqlite_dbptr virtual table modules to access pages
** of the database being recovered.
**
** This function always takes a single integer argument. If the arguement
** is zero, then the value returned is the number of pages in the db being
** recovered. If the argument is greater than zero, it is a page number.
** The value returned in this case is an SQL blob containing the data for
** the identified page of the db being recovered. e.g.
**
** SELECT getpage(0); -- return number of pages in db
** SELECT getpage(4); -- return page 4 of db as a blob of data
*/
static void recoverGetPage(
sqlite3_context *pCtx,
int nArg,
sqlite3_value **apArg
){
sqlite3_recover *p = (sqlite3_recover*)sqlite3_user_data(pCtx);
sqlite3_int64 pgno = sqlite3_value_int64(apArg[0]);
sqlite3_stmt *pStmt = 0;
assert( nArg==1 );
if( pgno==0 ){
pStmt = recoverPreparePrintf(p, p->dbIn, "PRAGMA %Q.page_count", p->zDb);
}else if( p->pGetPage==0 ){
pStmt = recoverPreparePrintf(
p, p->dbIn, "SELECT data FROM sqlite_dbpage(%Q) WHERE pgno=?", p->zDb
);
}else{
pStmt = p->pGetPage;
}
if( pStmt ){
if( pgno ) sqlite3_bind_int64(pStmt, 1, pgno);
if( SQLITE_ROW==sqlite3_step(pStmt) ){
sqlite3_result_value(pCtx, sqlite3_column_value(pStmt, 0));
}
if( pgno ){
p->pGetPage = recoverReset(p, pStmt);
}else{
recoverFinalize(p, pStmt);
}
}
if( p->errCode ){
if( p->zErrMsg ) sqlite3_result_error(pCtx, p->zErrMsg, -1);
sqlite3_result_error_code(pCtx, p->errCode);
}
}
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_dbdata_init(sqlite3*, char**, const sqlite3_api_routines*);
static int recoverOpenOutput(sqlite3_recover *p){
int rc = SQLITE_OK;
if( p->dbOut==0 ){
const int flags = SQLITE_OPEN_URI|SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE;
sqlite3 *db = 0;
assert( p->dbOut==0 );
rc = sqlite3_open_v2(p->zUri, &db, flags, 0);
if( rc==SQLITE_OK ){
const char *zPath = p->zStateDb ? p->zStateDb : ":memory:";
char *zSql = sqlite3_mprintf("ATTACH %Q AS recovery", zPath);
if( zSql==0 ){
rc = p->errCode = SQLITE_NOMEM;
}else{
rc = sqlite3_exec(db, zSql, 0, 0, 0);
}
sqlite3_free(zSql);
}
if( rc==SQLITE_OK ){
sqlite3_backup *pBackup = sqlite3_backup_init(db, "main", db, "recovery");
if( pBackup ){
while( sqlite3_backup_step(pBackup, 1000)==SQLITE_OK );
rc = sqlite3_backup_finish(pBackup);
}
}
if( rc==SQLITE_OK ){
rc = sqlite3_exec(db, RECOVERY_SCHEMA, 0, 0, 0);
}
if( rc==SQLITE_OK ){
sqlite3_dbdata_init(db, 0, 0);
rc = sqlite3_create_function(
db, "getpage", 1, SQLITE_UTF8, (void*)p, recoverGetPage, 0, 0
);
}
if( rc!=SQLITE_OK ){
if( p->errCode==SQLITE_OK ) rc = recoverDbError(p, db);
sqlite3_close(db);
}else{
p->dbOut = db;
}
}
return rc;
}
static int recoverCacheDbptr(sqlite3_recover *p){
return recoverExec(p, p->dbOut,
"INSERT INTO recovery.dbptr "
"SELECT pgno, child FROM sqlite_dbptr('getpage()')"
);
}
static int recoverCacheSchema(sqlite3_recover *p){
return recoverExec(p, p->dbOut,
"WITH RECURSIVE pages(p) AS ("
" SELECT 1"
" UNION"
" SELECT child FROM recovery.dbptr, pages WHERE pgno=p"
")"
"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('getpage()') WHERE pgno IN ("
" SELECT p FROM pages"
") GROUP BY pgno, cell"
);
}
static void recoverAddTable(sqlite3_recover *p, const char *zName, i64 iRoot){
sqlite3_stmt *pStmt = recoverPreparePrintf(p, p->dbOut,
"PRAGMA table_xinfo(%Q)", zName
);
if( pStmt ){
RecoverTable *pNew = 0;
int nCol = 0;
int nName = recoverStrlen(zName);
int nByte = 0;
while( sqlite3_step(pStmt)==SQLITE_ROW ){
nCol++;
nByte += (sqlite3_column_bytes(pStmt, 1)+1);
}
nByte += sizeof(RecoverTable) + nCol*sizeof(RecoverColumn) + nName+1;
recoverReset(p, pStmt);
pNew = recoverMalloc(p, nByte);
if( pNew ){
int i = 0;
char *csr = 0;
pNew->aCol = (RecoverColumn*)&pNew[1];
pNew->zTab = csr = (char*)&pNew->aCol[nCol];
pNew->nCol = nCol;
pNew->iRoot = iRoot;
pNew->iPk = -1;
memcpy(csr, zName, nName);
csr += nName+1;
for(i=0; sqlite3_step(pStmt)==SQLITE_ROW; i++){
int bPk = sqlite3_column_int(pStmt, 5);
int n = sqlite3_column_bytes(pStmt, 1);
const char *z = (const char*)sqlite3_column_text(pStmt, 1);
int eHidden = sqlite3_column_int(pStmt, 6);
if( bPk ) pNew->iPk = i;
pNew->aCol[i].zCol = csr;
pNew->aCol[i].eHidden = eHidden;
memcpy(csr, z, n);
csr += (n+1);
}
pNew->pNext = p->pTblList;
p->pTblList = pNew;
}
recoverFinalize(p, pStmt);
pStmt = recoverPreparePrintf(p, p->dbOut, "PRAGMA index_info(%Q)", zName);
if( pStmt && sqlite3_step(pStmt)!=SQLITE_ROW ){
pNew->bIntkey = 1;
}else{
pNew->iPk = -1;
}
recoverFinalize(p, pStmt);
}
}
/*
**
*/
static int recoverWriteSchema1(sqlite3_recover *p){
sqlite3_stmt *pSelect = 0;
sqlite3_stmt *pTblname = 0;
pSelect = recoverPrepare(p, p->dbOut,
"SELECT rootpage, sql, type='table' FROM recovery.schema "
" WHERE type='table' OR (type='index' AND sql LIKE '%unique%')"
);
pTblname = recoverPrepare(p, p->dbOut,
"SELECT name FROM sqlite_schema "
"WHERE type='table' ORDER BY rowid DESC LIMIT 1"
);
if( pSelect ){
while( sqlite3_step(pSelect)==SQLITE_ROW ){
i64 iRoot = sqlite3_column_int64(pSelect, 0);
const char *zSql = (const char*)sqlite3_column_text(pSelect, 1);
int bTable = sqlite3_column_int(pSelect, 2);
int rc = sqlite3_exec(p->dbOut, zSql, 0, 0, 0);
if( rc==SQLITE_OK ){
if( bTable ){
if( SQLITE_ROW==sqlite3_step(pTblname) ){
const char *zName = sqlite3_column_text(pTblname, 0);
recoverAddTable(p, zName, iRoot);
}
recoverReset(p, pTblname);
}
}else if( rc!=SQLITE_ERROR ){
recoverDbError(p, p->dbOut);
}
}
}
recoverFinalize(p, pSelect);
recoverFinalize(p, pTblname);
return p->errCode;
}
static int recoverWriteSchema2(sqlite3_recover *p){
sqlite3_stmt *pSelect = 0;
pSelect = recoverPrepare(p, p->dbOut,
"SELECT rootpage, sql FROM recovery.schema "
" WHERE type!='table' AND (type!='index' OR sql NOT LIKE '%unique%')"
);
if( pSelect ){
while( sqlite3_step(pSelect)==SQLITE_ROW ){
i64 iRoot = sqlite3_column_int64(pSelect, 0);
const char *zSql = (const char*)sqlite3_column_text(pSelect, 1);
int rc = sqlite3_exec(p->dbOut, zSql, 0, 0, 0);
if( rc!=SQLITE_OK && rc!=SQLITE_ERROR ){
recoverDbError(p, p->dbOut);
}
}
}
recoverFinalize(p, pSelect);
return p->errCode;
}
static char *recoverMPrintf(sqlite3_recover *p, const char *zFmt, ...){
char *zRet = 0;
if( p->errCode==SQLITE_OK ){
va_list ap;
char *z;
va_start(ap, zFmt);
zRet = sqlite3_vmprintf(zFmt, ap);
va_end(ap);
if( zRet==0 ){
p->errCode = SQLITE_NOMEM;
}
}
return zRet;
}
static sqlite3_stmt *recoverInsertStmt(
sqlite3_recover *p,
RecoverTable *pTab,
int nField
){
const char *zSep = "";
char *zSql = 0;
char *zBind = 0;
int ii;
sqlite3_stmt *pRet = 0;
assert( nField<=pTab->nCol );
zSql = recoverMPrintf(p, "INSERT OR IGNORE INTO %Q(", pTab->zTab);
for(ii=0; ii<nField; ii++){
int eHidden = pTab->aCol[ii].eHidden;
if( eHidden!=RECOVER_EHIDDEN_VIRTUAL
&& eHidden!=RECOVER_EHIDDEN_STORED
){
zSql = recoverMPrintf(p, "%z%s%Q", zSql, zSep, pTab->aCol[ii].zCol);
zBind = recoverMPrintf(p, "%z%s?", zBind, zSep);
zSep = ", ";
}
}
zSql = recoverMPrintf(p, "%z) VALUES (%z)", zSql, zBind);
pRet = recoverPrepare(p, p->dbOut, zSql);
sqlite3_free(zSql);
return pRet;
}
static RecoverTable *recoverFindTable(sqlite3_recover *p, u32 iRoot){
RecoverTable *pRet = 0;
for(pRet=p->pTblList; pRet && pRet->iRoot!=iRoot; pRet=pRet->pNext);
return pRet;
}
static int recoverWriteData(sqlite3_recover *p){
RecoverTable *pTbl;
int nMax = 0;
sqlite3_value **apVal = 0;
sqlite3_stmt *pSel = 0;
/* Figure out the maximum number of columns for any table in the schema */
for(pTbl=p->pTblList; pTbl; pTbl=pTbl->pNext){
if( pTbl->nCol>nMax ) nMax = pTbl->nCol;
}
apVal = (sqlite3_value**)recoverMalloc(p, sizeof(sqlite3_value*) * nMax);
if( apVal==0 ) return p->errCode;
pSel = recoverPrepare(p, p->dbOut,
"WITH RECURSIVE pages(root, page) AS ("
" SELECT rootpage, rootpage FROM recovery.schema"
" UNION"
" SELECT root, child FROM recovery.dbptr, pages WHERE pgno=page"
") "
"SELECT root, page, cell, field, value "
"FROM sqlite_dbdata('getpage()') d, pages p WHERE p.page=d.pgno "
"UNION ALL "
"SELECT 0, 0, 0, 0, 0"
);
if( pSel ){
RecoverTable *pTab = 0;
sqlite3_stmt *pInsert = 0;
int nInsert = -1;
i64 iPrevRoot = -1;
i64 iPrevPage = -1;
int iPrevCell = -1;
i64 iRowid = 0;
int nVal = -1;
while( p->errCode==SQLITE_OK && sqlite3_step(pSel)==SQLITE_ROW ){
i64 iRoot = sqlite3_column_int64(pSel, 0);
i64 iPage = sqlite3_column_int64(pSel, 1);
int iCell = sqlite3_column_int(pSel, 2);
int iField = sqlite3_column_int(pSel, 3);
sqlite3_value *pVal = sqlite3_column_value(pSel, 4);
int bNewCell = (iPrevRoot!=iRoot || iPrevPage!=iPage || iPrevCell!=iCell);
assert( bNewCell==0 || (iField==-1 || iField==0) );
assert( bNewCell || iField==nVal );
if( bNewCell ){
if( nVal>=0 ){
int ii;
if( pTab ){
int iVal = 0;
int iBind = 1;
if( pInsert==0 || nVal!=nInsert ){
recoverFinalize(p, pInsert);
pInsert = recoverInsertStmt(p, pTab, nVal);
nInsert = nVal;
}
for(ii=0; ii<pTab->nCol && iVal<nVal; ii++){
int eHidden = pTab->aCol[ii].eHidden;
switch( eHidden ){
case RECOVER_EHIDDEN_NONE:
case RECOVER_EHIDDEN_HIDDEN:
if( ii==pTab->iPk ){
sqlite3_bind_int64(pInsert, iBind, iRowid);
}else{
sqlite3_bind_value(pInsert, iBind, apVal[iVal]);
}
iBind++;
iVal++;
break;
case RECOVER_EHIDDEN_VIRTUAL:
break;
case RECOVER_EHIDDEN_STORED:
iVal++;
break;
}
}
sqlite3_step(pInsert);
recoverReset(p, pInsert);
assert( p->errCode || pInsert );
if( pInsert ) sqlite3_clear_bindings(pInsert);
}
for(ii=0; ii<nVal; ii++){
sqlite3_value_free(apVal[ii]);
apVal[ii] = 0;
}
nVal = -1;
}
if( iRoot==0 ) continue;
if( iRoot!=iPrevRoot ){
pTab = recoverFindTable(p, iRoot);
recoverFinalize(p, pInsert);
pInsert = 0;
}
}
if( iField<0 ){
iRowid = sqlite3_column_int64(pSel, 4);
assert( nVal==-1 );
nVal = 0;
}else if( iField<nMax ){
assert( apVal[iField]==0 );
apVal[iField] = sqlite3_value_dup( pVal );
nVal = iField+1;
}
iPrevRoot = iRoot;
iPrevCell = iCell;
iPrevPage = iPage;
}
recoverFinalize(p, pInsert);
recoverFinalize(p, pSel);
}
sqlite3_free(apVal);
return p->errCode;
}
sqlite3_recover *sqlite3_recover_init(
sqlite3* db,
const char *zDb,
const char *zUri
){
sqlite3_recover *pRet = 0;
int nDb = 0;
int nUri = 0;
int nByte = 0;
if( zDb==0 ){ zDb = "main"; }
if( zUri==0 ){ zUri = ""; }
nDb = recoverStrlen(zDb);
nUri = recoverStrlen(zUri);
nByte = sizeof(sqlite3_recover) + nDb+1 + nUri+1;
pRet = (sqlite3_recover*)sqlite3_malloc(nByte);
if( pRet ){
memset(pRet, 0, nByte);
pRet->dbIn = db;
pRet->zDb = (char*)&pRet[1];
pRet->zUri = &pRet->zDb[nDb+1];
memcpy(pRet->zDb, zDb, nDb);
memcpy(pRet->zUri, zUri, nUri);
}
return pRet;
}
const char *sqlite3_recover_errmsg(sqlite3_recover *p){
return p ? p->zErrMsg : "not an error";
}
int sqlite3_recover_errcode(sqlite3_recover *p){
return p ? p->errCode : SQLITE_NOMEM;
}
int sqlite3_recover_config(sqlite3_recover *p, int op, void *pArg){
int rc = SQLITE_OK;
switch( op ){
case SQLITE_RECOVER_TESTDB:
sqlite3_free(p->zStateDb);
p->zStateDb = sqlite3_mprintf("%s", (char*)pArg);
break;
default:
rc = SQLITE_NOTFOUND;
break;
}
return rc;
}
static void recoverStep(sqlite3_recover *p){
assert( p->errCode==SQLITE_OK );
if( p->dbOut==0 ){
if( recoverOpenOutput(p) ) return;
if( recoverCacheDbptr(p) ) return;
if( recoverCacheSchema(p) ) return;
if( recoverWriteSchema1(p) ) return;
if( recoverWriteData(p) ) return;
if( recoverWriteSchema2(p) ) return;
}
}
int sqlite3_recover_step(sqlite3_recover *p){
if( p && p->errCode==SQLITE_OK ){
recoverStep(p);
}
return p ? p->errCode : SQLITE_NOMEM;
}
int sqlite3_recover_finish(sqlite3_recover *p){
RecoverTable *pTab;
RecoverTable *pNext;
int rc;
for(pTab=p->pTblList; pTab; pTab=pNext){
pNext = pTab->pNext;
sqlite3_free(pTab);
}
sqlite3_finalize(p->pGetPage);
rc = sqlite3_close(p->dbOut);
assert( rc==SQLITE_OK );
p->pGetPage = 0;
rc = p->errCode;
sqlite3_free(p);
return rc;
}