Add the ".recover" command to the shell tool. For recovering as much data as possible from corrupt databases.
FossilOrigin-Name: 50fe48458942fa7a6bcc76316c6321f95b23dc34f2f8e0a483826483b2fb16f6
This commit is contained in:
commit
73c0d272a9
798
ext/misc/dbdata.c
Normal file
798
ext/misc/dbdata.c
Normal file
@ -0,0 +1,798 @@
|
|||||||
|
/*
|
||||||
|
** 2019-04-17
|
||||||
|
**
|
||||||
|
** 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.
|
||||||
|
**
|
||||||
|
******************************************************************************
|
||||||
|
**
|
||||||
|
** This file contains an implementation of two eponymous virtual tables,
|
||||||
|
** "sqlite_dbdata" and "sqlite_dbptr". Both modules require that the
|
||||||
|
** "sqlite_dbpage" eponymous virtual table be available.
|
||||||
|
**
|
||||||
|
** SQLITE_DBDATA:
|
||||||
|
** sqlite_dbdata is used to extract data directly from a database b-tree
|
||||||
|
** page and its associated overflow pages, bypassing the b-tree layer.
|
||||||
|
** The table schema is equivalent to:
|
||||||
|
**
|
||||||
|
** CREATE TABLE sqlite_dbdata(
|
||||||
|
** pgno INTEGER,
|
||||||
|
** cell INTEGER,
|
||||||
|
** field INTEGER,
|
||||||
|
** value ANY,
|
||||||
|
** schema TEXT HIDDEN
|
||||||
|
** );
|
||||||
|
**
|
||||||
|
** IMPORTANT: THE VIRTUAL TABLE SCHEMA ABOVE IS SUBJECT TO CHANGE. IN THE
|
||||||
|
** FUTURE NEW NON-HIDDEN COLUMNS MAY BE ADDED BETWEEN "value" AND
|
||||||
|
** "schema".
|
||||||
|
**
|
||||||
|
** Each page of the database is inspected. If it cannot be interpreted as
|
||||||
|
** a b-tree page, or if it is a b-tree page containing 0 entries, the
|
||||||
|
** sqlite_dbdata table contains no rows for that page. Otherwise, the
|
||||||
|
** table contains one row for each field in the record associated with
|
||||||
|
** each cell on the page. For intkey b-trees, the key value is stored in
|
||||||
|
** field -1.
|
||||||
|
**
|
||||||
|
** For example, for the database:
|
||||||
|
**
|
||||||
|
** CREATE TABLE t1(a, b); -- root page is page 2
|
||||||
|
** INSERT INTO t1(rowid, a, b) VALUES(5, 'v', 'five');
|
||||||
|
** INSERT INTO t1(rowid, a, b) VALUES(10, 'x', 'ten');
|
||||||
|
**
|
||||||
|
** the sqlite_dbdata table contains, as well as from entries related to
|
||||||
|
** page 1, content equivalent to:
|
||||||
|
**
|
||||||
|
** INSERT INTO sqlite_dbdata(pgno, cell, field, value) VALUES
|
||||||
|
** (2, 0, -1, 5 ),
|
||||||
|
** (2, 0, 0, 'v' ),
|
||||||
|
** (2, 0, 1, 'five'),
|
||||||
|
** (2, 1, -1, 10 ),
|
||||||
|
** (2, 1, 0, 'x' ),
|
||||||
|
** (2, 1, 1, 'ten' );
|
||||||
|
**
|
||||||
|
** If database corruption is encountered, this module does not report an
|
||||||
|
** error. Instead, it attempts to extract as much data as possible and
|
||||||
|
** ignores the corruption.
|
||||||
|
**
|
||||||
|
** SQLITE_DBPTR:
|
||||||
|
** The sqlite_dbptr table has the following schema:
|
||||||
|
**
|
||||||
|
** CREATE TABLE sqlite_dbptr(
|
||||||
|
** pgno INTEGER,
|
||||||
|
** child INTEGER,
|
||||||
|
** schema TEXT HIDDEN
|
||||||
|
** );
|
||||||
|
**
|
||||||
|
** It contains one entry for each b-tree pointer between a parent and
|
||||||
|
** child page in the database.
|
||||||
|
*/
|
||||||
|
#if !defined(SQLITEINT_H)
|
||||||
|
#include "sqlite3ext.h"
|
||||||
|
|
||||||
|
typedef unsigned char u8;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
SQLITE_EXTENSION_INIT1
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
typedef struct DbdataTable DbdataTable;
|
||||||
|
typedef struct DbdataCursor DbdataCursor;
|
||||||
|
|
||||||
|
/* Cursor object */
|
||||||
|
struct DbdataCursor {
|
||||||
|
sqlite3_vtab_cursor base; /* Base class. Must be first */
|
||||||
|
sqlite3_stmt *pStmt; /* For fetching database pages */
|
||||||
|
|
||||||
|
int iPgno; /* Current page number */
|
||||||
|
u8 *aPage; /* Buffer containing page */
|
||||||
|
int nPage; /* Size of aPage[] in bytes */
|
||||||
|
int nCell; /* Number of cells on aPage[] */
|
||||||
|
int iCell; /* Current cell number */
|
||||||
|
int bOnePage; /* True to stop after one page */
|
||||||
|
int szDb;
|
||||||
|
sqlite3_int64 iRowid;
|
||||||
|
|
||||||
|
/* Only for the sqlite_dbdata table */
|
||||||
|
u8 *pRec; /* Buffer containing current record */
|
||||||
|
int nRec; /* Size of pRec[] in bytes */
|
||||||
|
int nHdr; /* Size of header in bytes */
|
||||||
|
int iField; /* Current field number */
|
||||||
|
u8 *pHdrPtr;
|
||||||
|
u8 *pPtr;
|
||||||
|
|
||||||
|
sqlite3_int64 iIntkey; /* Integer key value */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Table object */
|
||||||
|
struct DbdataTable {
|
||||||
|
sqlite3_vtab base; /* Base class. Must be first */
|
||||||
|
sqlite3 *db; /* The database connection */
|
||||||
|
sqlite3_stmt *pStmt; /* For fetching database pages */
|
||||||
|
int bPtr; /* True for sqlite3_dbptr table */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Column and schema definitions for sqlite_dbdata */
|
||||||
|
#define DBDATA_COLUMN_PGNO 0
|
||||||
|
#define DBDATA_COLUMN_CELL 1
|
||||||
|
#define DBDATA_COLUMN_FIELD 2
|
||||||
|
#define DBDATA_COLUMN_VALUE 3
|
||||||
|
#define DBDATA_COLUMN_SCHEMA 4
|
||||||
|
#define DBDATA_SCHEMA \
|
||||||
|
"CREATE TABLE x(" \
|
||||||
|
" pgno INTEGER," \
|
||||||
|
" cell INTEGER," \
|
||||||
|
" field INTEGER," \
|
||||||
|
" value ANY," \
|
||||||
|
" schema TEXT HIDDEN" \
|
||||||
|
")"
|
||||||
|
|
||||||
|
/* Column and schema definitions for sqlite_dbptr */
|
||||||
|
#define DBPTR_COLUMN_PGNO 0
|
||||||
|
#define DBPTR_COLUMN_CHILD 1
|
||||||
|
#define DBPTR_COLUMN_SCHEMA 2
|
||||||
|
#define DBPTR_SCHEMA \
|
||||||
|
"CREATE TABLE x(" \
|
||||||
|
" pgno INTEGER," \
|
||||||
|
" child INTEGER," \
|
||||||
|
" schema TEXT HIDDEN" \
|
||||||
|
")"
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Connect to an sqlite_dbdata (pAux==0) or sqlite_dbptr (pAux!=0) virtual
|
||||||
|
** table.
|
||||||
|
*/
|
||||||
|
static int dbdataConnect(
|
||||||
|
sqlite3 *db,
|
||||||
|
void *pAux,
|
||||||
|
int argc, const char *const*argv,
|
||||||
|
sqlite3_vtab **ppVtab,
|
||||||
|
char **pzErr
|
||||||
|
){
|
||||||
|
DbdataTable *pTab = 0;
|
||||||
|
int rc = sqlite3_declare_vtab(db, pAux ? DBPTR_SCHEMA : DBDATA_SCHEMA);
|
||||||
|
|
||||||
|
if( rc==SQLITE_OK ){
|
||||||
|
pTab = (DbdataTable*)sqlite3_malloc64(sizeof(DbdataTable));
|
||||||
|
if( pTab==0 ){
|
||||||
|
rc = SQLITE_NOMEM;
|
||||||
|
}else{
|
||||||
|
memset(pTab, 0, sizeof(DbdataTable));
|
||||||
|
pTab->db = db;
|
||||||
|
pTab->bPtr = (pAux!=0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppVtab = (sqlite3_vtab*)pTab;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Disconnect from or destroy a sqlite_dbdata or sqlite_dbptr virtual table.
|
||||||
|
*/
|
||||||
|
static int dbdataDisconnect(sqlite3_vtab *pVtab){
|
||||||
|
DbdataTable *pTab = (DbdataTable*)pVtab;
|
||||||
|
if( pTab ){
|
||||||
|
sqlite3_finalize(pTab->pStmt);
|
||||||
|
sqlite3_free(pVtab);
|
||||||
|
}
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This function interprets two types of constraints:
|
||||||
|
**
|
||||||
|
** schema=?
|
||||||
|
** pgno=?
|
||||||
|
**
|
||||||
|
** If neither are present, idxNum is set to 0. If schema=? is present,
|
||||||
|
** the 0x01 bit in idxNum is set. If pgno=? is present, the 0x02 bit
|
||||||
|
** in idxNum is set.
|
||||||
|
**
|
||||||
|
** If both parameters are present, schema is in position 0 and pgno in
|
||||||
|
** position 1.
|
||||||
|
*/
|
||||||
|
static int dbdataBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdx){
|
||||||
|
DbdataTable *pTab = (DbdataTable*)tab;
|
||||||
|
int i;
|
||||||
|
int iSchema = -1;
|
||||||
|
int iPgno = -1;
|
||||||
|
int colSchema = (pTab->bPtr ? DBPTR_COLUMN_SCHEMA : DBDATA_COLUMN_SCHEMA);
|
||||||
|
|
||||||
|
for(i=0; i<pIdx->nConstraint; i++){
|
||||||
|
struct sqlite3_index_constraint *p = &pIdx->aConstraint[i];
|
||||||
|
if( p->op==SQLITE_INDEX_CONSTRAINT_EQ ){
|
||||||
|
if( p->iColumn==colSchema ){
|
||||||
|
if( p->usable==0 ) return SQLITE_CONSTRAINT;
|
||||||
|
iSchema = i;
|
||||||
|
}
|
||||||
|
if( p->iColumn==DBDATA_COLUMN_PGNO && p->usable ){
|
||||||
|
iPgno = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( iSchema>=0 ){
|
||||||
|
pIdx->aConstraintUsage[iSchema].argvIndex = 1;
|
||||||
|
pIdx->aConstraintUsage[iSchema].omit = 1;
|
||||||
|
}
|
||||||
|
if( iPgno>=0 ){
|
||||||
|
pIdx->aConstraintUsage[iPgno].argvIndex = 1 + (iSchema>=0);
|
||||||
|
pIdx->aConstraintUsage[iPgno].omit = 1;
|
||||||
|
pIdx->estimatedCost = 100;
|
||||||
|
pIdx->estimatedRows = 50;
|
||||||
|
|
||||||
|
if( pTab->bPtr==0 && pIdx->nOrderBy && pIdx->aOrderBy[0].desc==0 ){
|
||||||
|
int iCol = pIdx->aOrderBy[0].iColumn;
|
||||||
|
if( pIdx->nOrderBy==1 ){
|
||||||
|
pIdx->orderByConsumed = (iCol==0 || iCol==1);
|
||||||
|
}else if( pIdx->nOrderBy==2 && pIdx->aOrderBy[1].desc==0 && iCol==0 ){
|
||||||
|
pIdx->orderByConsumed = (pIdx->aOrderBy[1].iColumn==1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}else{
|
||||||
|
pIdx->estimatedCost = 100000000;
|
||||||
|
pIdx->estimatedRows = 1000000000;
|
||||||
|
}
|
||||||
|
pIdx->idxNum = (iSchema>=0 ? 0x01 : 0x00) | (iPgno>=0 ? 0x02 : 0x00);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Open a new sqlite_dbdata or sqlite_dbptr cursor.
|
||||||
|
*/
|
||||||
|
static int dbdataOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){
|
||||||
|
DbdataCursor *pCsr;
|
||||||
|
|
||||||
|
pCsr = (DbdataCursor*)sqlite3_malloc64(sizeof(DbdataCursor));
|
||||||
|
if( pCsr==0 ){
|
||||||
|
return SQLITE_NOMEM;
|
||||||
|
}else{
|
||||||
|
memset(pCsr, 0, sizeof(DbdataCursor));
|
||||||
|
pCsr->base.pVtab = pVTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppCursor = (sqlite3_vtab_cursor *)pCsr;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Restore a cursor object to the state it was in when first allocated
|
||||||
|
** by dbdataOpen().
|
||||||
|
*/
|
||||||
|
static void dbdataResetCursor(DbdataCursor *pCsr){
|
||||||
|
DbdataTable *pTab = (DbdataTable*)(pCsr->base.pVtab);
|
||||||
|
if( pTab->pStmt==0 ){
|
||||||
|
pTab->pStmt = pCsr->pStmt;
|
||||||
|
}else{
|
||||||
|
sqlite3_finalize(pCsr->pStmt);
|
||||||
|
}
|
||||||
|
pCsr->pStmt = 0;
|
||||||
|
pCsr->iPgno = 1;
|
||||||
|
pCsr->iCell = 0;
|
||||||
|
pCsr->iField = 0;
|
||||||
|
pCsr->bOnePage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Close an sqlite_dbdata or sqlite_dbptr cursor.
|
||||||
|
*/
|
||||||
|
static int dbdataClose(sqlite3_vtab_cursor *pCursor){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
dbdataResetCursor(pCsr);
|
||||||
|
sqlite3_free(pCsr);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Utility methods to decode 16 and 32-bit big-endian unsigned integers.
|
||||||
|
*/
|
||||||
|
static unsigned int get_uint16(unsigned char *a){
|
||||||
|
return (a[0]<<8)|a[1];
|
||||||
|
}
|
||||||
|
static unsigned int get_uint32(unsigned char *a){
|
||||||
|
return (a[0]<<24)|(a[1]<<16)|(a[2]<<8)|a[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Load page pgno from the database via the sqlite_dbpage virtual table.
|
||||||
|
** If successful, set (*ppPage) to point to a buffer containing the page
|
||||||
|
** data, (*pnPage) to the size of that buffer in bytes and return
|
||||||
|
** SQLITE_OK. In this case it is the responsibility of the caller to
|
||||||
|
** eventually free the buffer using sqlite3_free().
|
||||||
|
**
|
||||||
|
** Or, if an error occurs, set both (*ppPage) and (*pnPage) to 0 and
|
||||||
|
** return an SQLite error code.
|
||||||
|
*/
|
||||||
|
static int dbdataLoadPage(
|
||||||
|
DbdataCursor *pCsr, /* Cursor object */
|
||||||
|
unsigned int pgno, /* Page number of page to load */
|
||||||
|
u8 **ppPage, /* OUT: pointer to page buffer */
|
||||||
|
int *pnPage /* OUT: Size of (*ppPage) in bytes */
|
||||||
|
){
|
||||||
|
int rc2;
|
||||||
|
int rc = SQLITE_OK;
|
||||||
|
sqlite3_stmt *pStmt = pCsr->pStmt;
|
||||||
|
|
||||||
|
*ppPage = 0;
|
||||||
|
*pnPage = 0;
|
||||||
|
sqlite3_bind_int64(pStmt, 2, pgno);
|
||||||
|
if( SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||||
|
int nCopy = sqlite3_column_bytes(pStmt, 0);
|
||||||
|
if( nCopy>0 ){
|
||||||
|
u8 *pPage;
|
||||||
|
pPage = (u8*)sqlite3_malloc64(nCopy);
|
||||||
|
if( pPage==0 ){
|
||||||
|
rc = SQLITE_NOMEM;
|
||||||
|
}else{
|
||||||
|
const u8 *pCopy = sqlite3_column_blob(pStmt, 0);
|
||||||
|
memcpy(pPage, pCopy, nCopy);
|
||||||
|
}
|
||||||
|
*ppPage = pPage;
|
||||||
|
*pnPage = nCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc2 = sqlite3_reset(pStmt);
|
||||||
|
if( rc==SQLITE_OK ) rc = rc2;
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Read a varint. Put the value in *pVal and return the number of bytes.
|
||||||
|
*/
|
||||||
|
static int dbdataGetVarint(const u8 *z, sqlite3_int64 *pVal){
|
||||||
|
sqlite3_int64 v = 0;
|
||||||
|
int i;
|
||||||
|
for(i=0; i<8; i++){
|
||||||
|
v = (v<<7) + (z[i]&0x7f);
|
||||||
|
if( (z[i]&0x80)==0 ){ *pVal = v; return i+1; }
|
||||||
|
}
|
||||||
|
v = (v<<8) + (z[i]&0xff);
|
||||||
|
*pVal = v;
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Return the number of bytes of space used by an SQLite value of type
|
||||||
|
** eType.
|
||||||
|
*/
|
||||||
|
static int dbdataValueBytes(int eType){
|
||||||
|
switch( eType ){
|
||||||
|
case 0: case 8: case 9:
|
||||||
|
case 10: case 11:
|
||||||
|
return 0;
|
||||||
|
case 1:
|
||||||
|
return 1;
|
||||||
|
case 2:
|
||||||
|
return 2;
|
||||||
|
case 3:
|
||||||
|
return 3;
|
||||||
|
case 4:
|
||||||
|
return 4;
|
||||||
|
case 5:
|
||||||
|
return 6;
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
return 8;
|
||||||
|
default:
|
||||||
|
return ((eType-12) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Load a value of type eType from buffer pData and use it to set the
|
||||||
|
** result of context object pCtx.
|
||||||
|
*/
|
||||||
|
static void dbdataValue(sqlite3_context *pCtx, int eType, u8 *pData){
|
||||||
|
switch( eType ){
|
||||||
|
case 0:
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
sqlite3_result_null(pCtx);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
sqlite3_result_int(pCtx, 0);
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
sqlite3_result_int(pCtx, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: case 2: case 3: case 4: case 5: case 6: case 7: {
|
||||||
|
sqlite3_uint64 v = (signed char)pData[0];
|
||||||
|
pData++;
|
||||||
|
switch( eType ){
|
||||||
|
case 7:
|
||||||
|
case 6: v = (v<<16) + (pData[0]<<8) + pData[1]; pData += 2;
|
||||||
|
case 5: v = (v<<16) + (pData[0]<<8) + pData[1]; pData += 2;
|
||||||
|
case 4: v = (v<<8) + pData[0]; pData++;
|
||||||
|
case 3: v = (v<<8) + pData[0]; pData++;
|
||||||
|
case 2: v = (v<<8) + pData[0]; pData++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( eType==7 ){
|
||||||
|
double r;
|
||||||
|
memcpy(&r, &v, sizeof(r));
|
||||||
|
sqlite3_result_double(pCtx, r);
|
||||||
|
}else{
|
||||||
|
sqlite3_result_int64(pCtx, (sqlite3_int64)v);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
int n = ((eType-12) / 2);
|
||||||
|
if( eType % 2 ){
|
||||||
|
sqlite3_result_text(pCtx, (const char*)pData, n, SQLITE_TRANSIENT);
|
||||||
|
}else{
|
||||||
|
sqlite3_result_blob(pCtx, pData, n, SQLITE_TRANSIENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Move an sqlite_dbdata or sqlite_dbptr cursor to the next entry.
|
||||||
|
*/
|
||||||
|
static int dbdataNext(sqlite3_vtab_cursor *pCursor){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
|
||||||
|
|
||||||
|
pCsr->iRowid++;
|
||||||
|
while( 1 ){
|
||||||
|
int rc;
|
||||||
|
int iOff = (pCsr->iPgno==1 ? 100 : 0);
|
||||||
|
|
||||||
|
if( pCsr->aPage==0 ){
|
||||||
|
while( 1 ){
|
||||||
|
if( pCsr->bOnePage==0 && pCsr->iPgno>pCsr->szDb ) return SQLITE_OK;
|
||||||
|
rc = dbdataLoadPage(pCsr, pCsr->iPgno, &pCsr->aPage, &pCsr->nPage);
|
||||||
|
if( rc!=SQLITE_OK ) return rc;
|
||||||
|
if( pCsr->aPage ) break;
|
||||||
|
pCsr->iPgno++;
|
||||||
|
}
|
||||||
|
pCsr->iCell = pTab->bPtr ? -2 : 0;
|
||||||
|
pCsr->nCell = get_uint16(&pCsr->aPage[iOff+3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( pTab->bPtr ){
|
||||||
|
if( pCsr->aPage[iOff]!=0x02 && pCsr->aPage[iOff]!=0x05 ){
|
||||||
|
pCsr->iCell = pCsr->nCell;
|
||||||
|
}
|
||||||
|
pCsr->iCell++;
|
||||||
|
if( pCsr->iCell>=pCsr->nCell ){
|
||||||
|
sqlite3_free(pCsr->aPage);
|
||||||
|
pCsr->aPage = 0;
|
||||||
|
if( pCsr->bOnePage ) return SQLITE_OK;
|
||||||
|
pCsr->iPgno++;
|
||||||
|
}else{
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
/* If there is no record loaded, load it now. */
|
||||||
|
if( pCsr->pRec==0 ){
|
||||||
|
int bHasRowid = 0;
|
||||||
|
int nPointer = 0;
|
||||||
|
sqlite3_int64 nPayload = 0;
|
||||||
|
sqlite3_int64 nHdr = 0;
|
||||||
|
int iHdr;
|
||||||
|
int U, X;
|
||||||
|
int nLocal;
|
||||||
|
|
||||||
|
switch( pCsr->aPage[iOff] ){
|
||||||
|
case 0x02:
|
||||||
|
nPointer = 4;
|
||||||
|
break;
|
||||||
|
case 0x0a:
|
||||||
|
break;
|
||||||
|
case 0x0d:
|
||||||
|
bHasRowid = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/* This is not a b-tree page with records on it. Continue. */
|
||||||
|
pCsr->iCell = pCsr->nCell;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( pCsr->iCell>=pCsr->nCell ){
|
||||||
|
sqlite3_free(pCsr->aPage);
|
||||||
|
pCsr->aPage = 0;
|
||||||
|
if( pCsr->bOnePage ) return SQLITE_OK;
|
||||||
|
pCsr->iPgno++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
iOff += 8 + nPointer + pCsr->iCell*2;
|
||||||
|
iOff = get_uint16(&pCsr->aPage[iOff]);
|
||||||
|
|
||||||
|
/* For an interior node cell, skip past the child-page number */
|
||||||
|
iOff += nPointer;
|
||||||
|
|
||||||
|
/* Load the "byte of payload including overflow" field */
|
||||||
|
iOff += dbdataGetVarint(&pCsr->aPage[iOff], &nPayload);
|
||||||
|
|
||||||
|
/* If this is a leaf intkey cell, load the rowid */
|
||||||
|
if( bHasRowid ){
|
||||||
|
iOff += dbdataGetVarint(&pCsr->aPage[iOff], &pCsr->iIntkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allocate space for payload */
|
||||||
|
pCsr->pRec = (u8*)sqlite3_malloc64(nPayload);
|
||||||
|
if( pCsr->pRec==0 ) return SQLITE_NOMEM;
|
||||||
|
pCsr->nRec = nPayload;
|
||||||
|
|
||||||
|
U = pCsr->nPage;
|
||||||
|
if( bHasRowid ){
|
||||||
|
X = U-35;
|
||||||
|
}else{
|
||||||
|
X = ((U-12)*64/255)-23;
|
||||||
|
}
|
||||||
|
if( nPayload<=X ){
|
||||||
|
nLocal = nPayload;
|
||||||
|
}else{
|
||||||
|
int M, K;
|
||||||
|
M = ((U-12)*32/255)-23;
|
||||||
|
K = M+((nPayload-M)%(U-4));
|
||||||
|
if( K<=X ){
|
||||||
|
nLocal = K;
|
||||||
|
}else{
|
||||||
|
nLocal = M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Load the nLocal bytes of payload */
|
||||||
|
memcpy(pCsr->pRec, &pCsr->aPage[iOff], nLocal);
|
||||||
|
iOff += nLocal;
|
||||||
|
|
||||||
|
/* Load content from overflow pages */
|
||||||
|
if( nPayload>nLocal ){
|
||||||
|
sqlite3_int64 nRem = nPayload - nLocal;
|
||||||
|
unsigned int pgnoOvfl = get_uint32(&pCsr->aPage[iOff]);
|
||||||
|
while( nRem>0 ){
|
||||||
|
u8 *aOvfl = 0;
|
||||||
|
int nOvfl = 0;
|
||||||
|
int nCopy;
|
||||||
|
rc = dbdataLoadPage(pCsr, pgnoOvfl, &aOvfl, &nOvfl);
|
||||||
|
assert( rc!=SQLITE_OK || nOvfl==pCsr->nPage );
|
||||||
|
if( rc!=SQLITE_OK ) return rc;
|
||||||
|
|
||||||
|
nCopy = U-4;
|
||||||
|
if( nCopy>nRem ) nCopy = nRem;
|
||||||
|
memcpy(&pCsr->pRec[nPayload-nRem], &aOvfl[4], nCopy);
|
||||||
|
nRem -= nCopy;
|
||||||
|
|
||||||
|
pgnoOvfl = get_uint32(aOvfl);
|
||||||
|
sqlite3_free(aOvfl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iHdr = dbdataGetVarint(pCsr->pRec, &nHdr);
|
||||||
|
pCsr->nHdr = nHdr;
|
||||||
|
pCsr->pHdrPtr = &pCsr->pRec[iHdr];
|
||||||
|
pCsr->pPtr = &pCsr->pRec[pCsr->nHdr];
|
||||||
|
pCsr->iField = (bHasRowid ? -1 : 0);
|
||||||
|
}else{
|
||||||
|
pCsr->iField++;
|
||||||
|
if( pCsr->iField>0 ){
|
||||||
|
sqlite3_int64 iType;
|
||||||
|
pCsr->pHdrPtr += dbdataGetVarint(pCsr->pHdrPtr, &iType);
|
||||||
|
pCsr->pPtr += dbdataValueBytes(iType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( pCsr->iField<0 || pCsr->pHdrPtr<&pCsr->pRec[pCsr->nHdr] ){
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Advance to the next cell. The next iteration of the loop will load
|
||||||
|
** the record and so on. */
|
||||||
|
sqlite3_free(pCsr->pRec);
|
||||||
|
pCsr->pRec = 0;
|
||||||
|
pCsr->iCell++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert( !"can't get here" );
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Return true if the cursor is at EOF.
|
||||||
|
*/
|
||||||
|
static int dbdataEof(sqlite3_vtab_cursor *pCursor){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
return pCsr->aPage==0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Determine the size in pages of database zSchema (where zSchema is
|
||||||
|
** "main", "temp" or the name of an attached database) and set
|
||||||
|
** pCsr->szDb accordingly. If successful, return SQLITE_OK. Otherwise,
|
||||||
|
** an SQLite error code.
|
||||||
|
*/
|
||||||
|
static int dbdataDbsize(DbdataCursor *pCsr, const char *zSchema){
|
||||||
|
DbdataTable *pTab = (DbdataTable*)pCsr->base.pVtab;
|
||||||
|
char *zSql = 0;
|
||||||
|
int rc, rc2;
|
||||||
|
sqlite3_stmt *pStmt = 0;
|
||||||
|
|
||||||
|
zSql = sqlite3_mprintf("PRAGMA %Q.page_count", zSchema);
|
||||||
|
if( zSql==0 ) return SQLITE_NOMEM;
|
||||||
|
rc = sqlite3_prepare_v2(pTab->db, zSql, -1, &pStmt, 0);
|
||||||
|
sqlite3_free(zSql);
|
||||||
|
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
|
||||||
|
pCsr->szDb = sqlite3_column_int(pStmt, 0);
|
||||||
|
}
|
||||||
|
rc2 = sqlite3_finalize(pStmt);
|
||||||
|
if( rc==SQLITE_OK ) rc = rc2;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** xFilter method for sqlite_dbdata and sqlite_dbptr.
|
||||||
|
*/
|
||||||
|
static int dbdataFilter(
|
||||||
|
sqlite3_vtab_cursor *pCursor,
|
||||||
|
int idxNum, const char *idxStr,
|
||||||
|
int argc, sqlite3_value **argv
|
||||||
|
){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
|
||||||
|
int rc = SQLITE_OK;
|
||||||
|
const char *zSchema = "main";
|
||||||
|
|
||||||
|
dbdataResetCursor(pCsr);
|
||||||
|
assert( pCsr->iPgno==1 );
|
||||||
|
if( idxNum & 0x01 ){
|
||||||
|
zSchema = (const char*)sqlite3_value_text(argv[0]);
|
||||||
|
}
|
||||||
|
if( idxNum & 0x02 ){
|
||||||
|
pCsr->iPgno = sqlite3_value_int(argv[(idxNum & 0x01)]);
|
||||||
|
pCsr->bOnePage = 1;
|
||||||
|
}else{
|
||||||
|
pCsr->nPage = dbdataDbsize(pCsr, zSchema);
|
||||||
|
rc = dbdataDbsize(pCsr, zSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( rc==SQLITE_OK ){
|
||||||
|
if( pTab->pStmt ){
|
||||||
|
pCsr->pStmt = pTab->pStmt;
|
||||||
|
pTab->pStmt = 0;
|
||||||
|
}else{
|
||||||
|
rc = sqlite3_prepare_v2(pTab->db,
|
||||||
|
"SELECT data FROM sqlite_dbpage(?) WHERE pgno=?", -1,
|
||||||
|
&pCsr->pStmt, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if( rc==SQLITE_OK ){
|
||||||
|
rc = sqlite3_bind_text(pCsr->pStmt, 1, zSchema, -1, SQLITE_TRANSIENT);
|
||||||
|
}else{
|
||||||
|
pTab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db));
|
||||||
|
}
|
||||||
|
if( rc==SQLITE_OK ){
|
||||||
|
rc = dbdataNext(pCursor);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Return a column for the sqlite_dbdata or sqlite_dbptr table.
|
||||||
|
*/
|
||||||
|
static int dbdataColumn(
|
||||||
|
sqlite3_vtab_cursor *pCursor,
|
||||||
|
sqlite3_context *ctx,
|
||||||
|
int i
|
||||||
|
){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
|
||||||
|
if( pTab->bPtr ){
|
||||||
|
switch( i ){
|
||||||
|
case DBPTR_COLUMN_PGNO:
|
||||||
|
sqlite3_result_int64(ctx, pCsr->iPgno);
|
||||||
|
break;
|
||||||
|
case DBPTR_COLUMN_CHILD: {
|
||||||
|
int iOff = pCsr->iPgno==1 ? 100 : 0;
|
||||||
|
if( pCsr->iCell<0 ){
|
||||||
|
iOff += 8;
|
||||||
|
}else{
|
||||||
|
iOff += 12 + pCsr->iCell*2;
|
||||||
|
iOff = get_uint16(&pCsr->aPage[iOff]);
|
||||||
|
}
|
||||||
|
sqlite3_result_int64(ctx, get_uint32(&pCsr->aPage[iOff]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
switch( i ){
|
||||||
|
case DBDATA_COLUMN_PGNO:
|
||||||
|
sqlite3_result_int64(ctx, pCsr->iPgno);
|
||||||
|
break;
|
||||||
|
case DBDATA_COLUMN_CELL:
|
||||||
|
sqlite3_result_int(ctx, pCsr->iCell);
|
||||||
|
break;
|
||||||
|
case DBDATA_COLUMN_FIELD:
|
||||||
|
sqlite3_result_int(ctx, pCsr->iField);
|
||||||
|
break;
|
||||||
|
case DBDATA_COLUMN_VALUE: {
|
||||||
|
if( pCsr->iField<0 ){
|
||||||
|
sqlite3_result_int64(ctx, pCsr->iIntkey);
|
||||||
|
}else{
|
||||||
|
sqlite3_int64 iType;
|
||||||
|
dbdataGetVarint(pCsr->pHdrPtr, &iType);
|
||||||
|
dbdataValue(ctx, iType, pCsr->pPtr);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Return the rowid for an sqlite_dbdata or sqlite_dptr table.
|
||||||
|
*/
|
||||||
|
static int dbdataRowid(sqlite3_vtab_cursor *pCursor, sqlite_int64 *pRowid){
|
||||||
|
DbdataCursor *pCsr = (DbdataCursor*)pCursor;
|
||||||
|
*pRowid = pCsr->iRowid;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Invoke this routine to register the "sqlite_dbdata" virtual table module
|
||||||
|
*/
|
||||||
|
static int sqlite3DbdataRegister(sqlite3 *db){
|
||||||
|
static sqlite3_module dbdata_module = {
|
||||||
|
0, /* iVersion */
|
||||||
|
0, /* xCreate */
|
||||||
|
dbdataConnect, /* xConnect */
|
||||||
|
dbdataBestIndex, /* xBestIndex */
|
||||||
|
dbdataDisconnect, /* xDisconnect */
|
||||||
|
0, /* xDestroy */
|
||||||
|
dbdataOpen, /* xOpen - open a cursor */
|
||||||
|
dbdataClose, /* xClose - close a cursor */
|
||||||
|
dbdataFilter, /* xFilter - configure scan constraints */
|
||||||
|
dbdataNext, /* xNext - advance a cursor */
|
||||||
|
dbdataEof, /* xEof - check for end of scan */
|
||||||
|
dbdataColumn, /* xColumn - read data */
|
||||||
|
dbdataRowid, /* xRowid - read data */
|
||||||
|
0, /* xUpdate */
|
||||||
|
0, /* xBegin */
|
||||||
|
0, /* xSync */
|
||||||
|
0, /* xCommit */
|
||||||
|
0, /* xRollback */
|
||||||
|
0, /* xFindMethod */
|
||||||
|
0, /* xRename */
|
||||||
|
0, /* xSavepoint */
|
||||||
|
0, /* xRelease */
|
||||||
|
0, /* xRollbackTo */
|
||||||
|
0 /* xShadowName */
|
||||||
|
};
|
||||||
|
|
||||||
|
int rc = sqlite3_create_module(db, "sqlite_dbdata", &dbdata_module, 0);
|
||||||
|
if( rc==SQLITE_OK ){
|
||||||
|
rc = sqlite3_create_module(db, "sqlite_dbptr", &dbdata_module, (void*)1);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
__declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
int sqlite3_dbdata_init(
|
||||||
|
sqlite3 *db,
|
||||||
|
char **pzErrMsg,
|
||||||
|
const sqlite3_api_routines *pApi
|
||||||
|
){
|
||||||
|
SQLITE_EXTENSION_INIT2(pApi);
|
||||||
|
return sqlite3DbdataRegister(db);
|
||||||
|
}
|
1
main.mk
1
main.mk
@ -738,6 +738,7 @@ SHELL_SRC = \
|
|||||||
$(TOP)/ext/expert/sqlite3expert.h \
|
$(TOP)/ext/expert/sqlite3expert.h \
|
||||||
$(TOP)/ext/misc/zipfile.c \
|
$(TOP)/ext/misc/zipfile.c \
|
||||||
$(TOP)/ext/misc/memtrace.c \
|
$(TOP)/ext/misc/memtrace.c \
|
||||||
|
$(TOP)/ext/misc/dbdata.c \
|
||||||
$(TOP)/src/test_windirent.c
|
$(TOP)/src/test_windirent.c
|
||||||
|
|
||||||
shell.c: $(SHELL_SRC) $(TOP)/tool/mkshellc.tcl
|
shell.c: $(SHELL_SRC) $(TOP)/tool/mkshellc.tcl
|
||||||
|
22
manifest
22
manifest
@ -1,5 +1,5 @@
|
|||||||
C Fix\sa\sminor\stypo\sin\sa\scomment.\s\sNo\schanges\sto\scode.
|
C Add\sthe\s".recover"\scommand\sto\sthe\sshell\stool.\sFor\srecovering\sas\smuch\sdata\sas\spossible\sfrom\scorrupt\sdatabases.
|
||||||
D 2019-04-27T20:16:42.497
|
D 2019-04-27T20:30:19.423
|
||||||
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
||||||
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
||||||
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
|
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
|
||||||
@ -284,6 +284,7 @@ F ext/misc/closure.c dbfd8543b2a017ae6b1a5843986b22ddf99ff126ec9634a2f4047cd14c8
|
|||||||
F ext/misc/completion.c cec672d40604075bb341a7f11ac48393efdcd90a979269b8fe7977ea62d0547f
|
F ext/misc/completion.c cec672d40604075bb341a7f11ac48393efdcd90a979269b8fe7977ea62d0547f
|
||||||
F ext/misc/compress.c dd4f8a6d0baccff3c694757db5b430f3bbd821d8686d1fc24df55cf9f035b189
|
F ext/misc/compress.c dd4f8a6d0baccff3c694757db5b430f3bbd821d8686d1fc24df55cf9f035b189
|
||||||
F ext/misc/csv.c 7f047aeb68f5802e7ce6639292095d622a488bb43526ed04810e0649faa71ceb
|
F ext/misc/csv.c 7f047aeb68f5802e7ce6639292095d622a488bb43526ed04810e0649faa71ceb
|
||||||
|
F ext/misc/dbdata.c 1b3751b02d8f575d25c6bda358670d2e39ace368a0d05595989c308a10c615f6
|
||||||
F ext/misc/dbdump.c baf6e37447c9d6968417b1cd34cbedb0b0ab3f91b5329501d8a8d5be3287c336
|
F ext/misc/dbdump.c baf6e37447c9d6968417b1cd34cbedb0b0ab3f91b5329501d8a8d5be3287c336
|
||||||
F ext/misc/eval.c 4b4757592d00fd32e44c7a067e6a0e4839c81a4d57abc4131ee7806d1be3104e
|
F ext/misc/eval.c 4b4757592d00fd32e44c7a067e6a0e4839c81a4d57abc4131ee7806d1be3104e
|
||||||
F ext/misc/explain.c d5c12962d79913ef774b297006872af1fccda388f61a11d37758f9179a09551f
|
F ext/misc/explain.c d5c12962d79913ef774b297006872af1fccda388f61a11d37758f9179a09551f
|
||||||
@ -440,7 +441,7 @@ F ext/userauth/userauth.c f81aa5a3ecacf406f170c62a144405858f6f6de51dbdc0920134e6
|
|||||||
F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x
|
F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x
|
||||||
F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
|
F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
|
||||||
F magic.txt 8273bf49ba3b0c8559cb2774495390c31fd61c60
|
F magic.txt 8273bf49ba3b0c8559cb2774495390c31fd61c60
|
||||||
F main.mk 23d3660f7053d196aef76938bf78b10fc3ce1831a85d96bd71565758788f34d4
|
F main.mk 125adda36bb32c99dc3a11340bd029ef373b9523eac2b2af76087bfe82d4fdf8
|
||||||
F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83
|
F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83
|
||||||
F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271
|
F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271
|
||||||
F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504
|
F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504
|
||||||
@ -519,7 +520,7 @@ F src/random.c 80f5d666f23feb3e6665a6ce04c7197212a88384
|
|||||||
F src/resolve.c 567888ee3faec14dae06519b4306201771058364a37560186a3e0e755ebc4cb8
|
F src/resolve.c 567888ee3faec14dae06519b4306201771058364a37560186a3e0e755ebc4cb8
|
||||||
F src/rowset.c d977b011993aaea002cab3e0bb2ce50cf346000dff94e944d547b989f4b1fe93
|
F src/rowset.c d977b011993aaea002cab3e0bb2ce50cf346000dff94e944d547b989f4b1fe93
|
||||||
F src/select.c b7304d2f491c11a03a7fbdf34bc218282ac54052377809d4dc3b4b1e7f4bfc93
|
F src/select.c b7304d2f491c11a03a7fbdf34bc218282ac54052377809d4dc3b4b1e7f4bfc93
|
||||||
F src/shell.c.in bcfa17eb257bf8dc2359e99ba7e6bdfab7901705db013bc47a5be6d7fa7a037e
|
F src/shell.c.in 104bbae904a2b67bc6c0c95337447544d15d0594dc46468608aae769d5f51da9
|
||||||
F src/sqlite.h.in 38390767acc1914d58930e03149595ee4710afa4e3c43ab6c3a8aea3f1a6b8cd
|
F src/sqlite.h.in 38390767acc1914d58930e03149595ee4710afa4e3c43ab6c3a8aea3f1a6b8cd
|
||||||
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
|
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
|
||||||
F src/sqlite3ext.h 9ecc93b8493bd20c0c07d52e2ac0ed8bab9b549c7f7955b59869597b650dd8b5
|
F src/sqlite3ext.h 9ecc93b8493bd20c0c07d52e2ac0ed8bab9b549c7f7955b59869597b650dd8b5
|
||||||
@ -786,6 +787,7 @@ F test/cursorhint2.test 6f3aa9cb19e7418967a10ec6905209bcbb5968054da855fc36c8beee
|
|||||||
F test/dataversion1.test 6e5e86ac681f0782e766ebcb56c019ae001522d114e0e111e5ebf68ccf2a7bb8
|
F test/dataversion1.test 6e5e86ac681f0782e766ebcb56c019ae001522d114e0e111e5ebf68ccf2a7bb8
|
||||||
F test/date.test 9b73bbeb1b82d9c1f44dec5cf563bf7da58d2373
|
F test/date.test 9b73bbeb1b82d9c1f44dec5cf563bf7da58d2373
|
||||||
F test/date2.test 74c234bece1b016e94dd4ef9c8cc7a199a8806c0e2291cab7ba64bace6350b10
|
F test/date2.test 74c234bece1b016e94dd4ef9c8cc7a199a8806c0e2291cab7ba64bace6350b10
|
||||||
|
F test/dbdata.test c8d97bafd1b2efb1e445871c4641208dcd91e686d2dfbb6463d83934adbd1ac5
|
||||||
F test/dbfuzz.c 73047c920d6210e5912c87cdffd9a1c281d4252e
|
F test/dbfuzz.c 73047c920d6210e5912c87cdffd9a1c281d4252e
|
||||||
F test/dbfuzz001.test e32d14465f1c77712896fda6a1ccc0f037b481c191c1696a9c44f6c9e4964faf
|
F test/dbfuzz001.test e32d14465f1c77712896fda6a1ccc0f037b481c191c1696a9c44f6c9e4964faf
|
||||||
F test/dbfuzz2-seed1.db e6225c6f3d7b63f9c5b6867146a5f329d997ab105bee64644dc2b3a2f2aebaee
|
F test/dbfuzz2-seed1.db e6225c6f3d7b63f9c5b6867146a5f329d997ab105bee64644dc2b3a2f2aebaee
|
||||||
@ -1224,6 +1226,7 @@ F test/randexpr1.tcl 40dec52119ed3a2b8b2a773bce24b63a3a746459
|
|||||||
F test/randexpr1.test eda062a97e60f9c38ae8d806b03b0ddf23d796df
|
F test/randexpr1.test eda062a97e60f9c38ae8d806b03b0ddf23d796df
|
||||||
F test/rbu.test 168573d353cd0fd10196b87b0caa322c144ef736
|
F test/rbu.test 168573d353cd0fd10196b87b0caa322c144ef736
|
||||||
F test/rdonly.test 64e2696c322e3538df0b1ed624e21f9a23ed9ff8
|
F test/rdonly.test 64e2696c322e3538df0b1ed624e21f9a23ed9ff8
|
||||||
|
F test/recover.test 52609c8cc24e72d3d8a20fb8bc32ba2ce8ca2093a7f4573bd4f2969f78f6d2b4
|
||||||
F test/regexp1.test 497ea812f264d12b6198d6e50a76be4a1973a9d8
|
F test/regexp1.test 497ea812f264d12b6198d6e50a76be4a1973a9d8
|
||||||
F test/regexp2.test 40e894223b3d6672655481493f1be12012f2b33c
|
F test/regexp2.test 40e894223b3d6672655481493f1be12012f2b33c
|
||||||
F test/reindex.test 44edd3966b474468b823d481eafef0c305022254
|
F test/reindex.test 44edd3966b474468b823d481eafef0c305022254
|
||||||
@ -1752,7 +1755,7 @@ F tool/mkopcodec.tcl d1b6362bd3aa80d5520d4d6f3765badf01f6c43c
|
|||||||
F tool/mkopcodeh.tcl 352a4319c0ad869eb26442bf7c3b015aa15594c21f1cce5a6420dbe999367c21
|
F tool/mkopcodeh.tcl 352a4319c0ad869eb26442bf7c3b015aa15594c21f1cce5a6420dbe999367c21
|
||||||
F tool/mkopts.tcl 680f785fdb09729fd9ac50632413da4eadbdf9071535e3f26d03795828ab07fa
|
F tool/mkopts.tcl 680f785fdb09729fd9ac50632413da4eadbdf9071535e3f26d03795828ab07fa
|
||||||
F tool/mkpragmatab.tcl 49039adedafbc430d2959400da2e0e8f20ef8dcf6898e447c946e7d50ef5906b
|
F tool/mkpragmatab.tcl 49039adedafbc430d2959400da2e0e8f20ef8dcf6898e447c946e7d50ef5906b
|
||||||
F tool/mkshellc.tcl 1f45770aea226ac093a9c72f718efbb88a2a2833409ec2e1c4cecae4202626f5
|
F tool/mkshellc.tcl 70a9978e363b0f3280ca9ce1c46d72563ff479c1930a12a7375e3881b7325712
|
||||||
F tool/mksourceid.c d458f9004c837bee87a6382228ac20d3eae3c49ea3b0a5aace936f8b60748d3b
|
F tool/mksourceid.c d458f9004c837bee87a6382228ac20d3eae3c49ea3b0a5aace936f8b60748d3b
|
||||||
F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97
|
F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97
|
||||||
F tool/mksqlite3c-noext.tcl 4f7cfef5152b0c91920355cbfc1d608a4ad242cb819f1aea07f6d0274f584a7f
|
F tool/mksqlite3c-noext.tcl 4f7cfef5152b0c91920355cbfc1d608a4ad242cb819f1aea07f6d0274f584a7f
|
||||||
@ -1818,7 +1821,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
|
|||||||
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
||||||
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
||||||
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
||||||
P af53c41a127c314c0608f3fd016d3a26896783745e46cd180976a188400cdb75
|
P 95209072176ff21a91e96d5bd014b35ef100da2b0b93958baf6df4294a8daa85 425d708c3908fe74f69b62e6dd1722a0018088977e12f14b312dad1df0fbb804
|
||||||
R 5d8e72521d268ff442af4eda367a86ab
|
R f359cf293ea45304e0ddadd16c7a1206
|
||||||
U drh
|
T +closed 425d708c3908fe74f69b62e6dd1722a0018088977e12f14b312dad1df0fbb804
|
||||||
Z c27e4c4c6167f0761ffa85b745f3dbd7
|
U dan
|
||||||
|
Z ee7f2d083d1fca64ba3bb33b40a19208
|
||||||
|
@ -1 +1 @@
|
|||||||
95209072176ff21a91e96d5bd014b35ef100da2b0b93958baf6df4294a8daa85
|
50fe48458942fa7a6bcc76316c6321f95b23dc34f2f8e0a483826483b2fb16f6
|
780
src/shell.c.in
780
src/shell.c.in
@ -948,6 +948,10 @@ INCLUDE ../ext/misc/sqlar.c
|
|||||||
INCLUDE ../ext/expert/sqlite3expert.h
|
INCLUDE ../ext/expert/sqlite3expert.h
|
||||||
INCLUDE ../ext/expert/sqlite3expert.c
|
INCLUDE ../ext/expert/sqlite3expert.c
|
||||||
|
|
||||||
|
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||||
|
INCLUDE ../ext/misc/dbdata.c
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(SQLITE_ENABLE_SESSION)
|
#if defined(SQLITE_ENABLE_SESSION)
|
||||||
/*
|
/*
|
||||||
** State information for a single open session
|
** State information for a single open session
|
||||||
@ -3574,6 +3578,9 @@ static const char *(azHelp[]) = {
|
|||||||
".prompt MAIN CONTINUE Replace the standard prompts",
|
".prompt MAIN CONTINUE Replace the standard prompts",
|
||||||
".quit Exit this program",
|
".quit Exit this program",
|
||||||
".read FILE Read input from FILE",
|
".read FILE Read input from FILE",
|
||||||
|
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||||
|
".recover Recover as much data as possible from corrupt db.",
|
||||||
|
#endif
|
||||||
".restore ?DB? FILE Restore content of DB (default \"main\") from FILE",
|
".restore ?DB? FILE Restore content of DB (default \"main\") from FILE",
|
||||||
".save FILE Write in-memory database into FILE",
|
".save FILE Write in-memory database into FILE",
|
||||||
".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off",
|
".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off",
|
||||||
@ -3931,6 +3938,125 @@ readHexDb_error:
|
|||||||
}
|
}
|
||||||
#endif /* SQLITE_ENABLE_DESERIALIZE */
|
#endif /* SQLITE_ENABLE_DESERIALIZE */
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Scalar function "shell_int32". The first argument to this function
|
||||||
|
** must be a blob. The second a non-negative integer. This function
|
||||||
|
** reads and returns a 32-bit big-endian integer from byte
|
||||||
|
** offset (4*<arg2>) of the blob.
|
||||||
|
*/
|
||||||
|
static void shellInt32(
|
||||||
|
sqlite3_context *context,
|
||||||
|
int argc,
|
||||||
|
sqlite3_value **argv
|
||||||
|
){
|
||||||
|
const unsigned char *pBlob;
|
||||||
|
int nBlob;
|
||||||
|
int iInt;
|
||||||
|
|
||||||
|
nBlob = sqlite3_value_bytes(argv[0]);
|
||||||
|
pBlob = (const unsigned char*)sqlite3_value_blob(argv[0]);
|
||||||
|
iInt = sqlite3_value_int(argv[1]);
|
||||||
|
|
||||||
|
if( iInt>=0 && (iInt+1)*4<=nBlob ){
|
||||||
|
const unsigned char *a = &pBlob[iInt*4];
|
||||||
|
sqlite3_int64 iVal = ((sqlite3_int64)a[0]<<24)
|
||||||
|
+ ((sqlite3_int64)a[1]<<16)
|
||||||
|
+ ((sqlite3_int64)a[2]<< 8)
|
||||||
|
+ ((sqlite3_int64)a[3]<< 0);
|
||||||
|
sqlite3_result_int64(context, iVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Scalar function "shell_escape_crnl" used by the .recover command.
|
||||||
|
** The argument passed to this function is the output of built-in
|
||||||
|
** function quote(). If the first character of the input is "'",
|
||||||
|
** indicating that the value passed to quote() was a text value,
|
||||||
|
** then this function searches the input for "\n" and "\r" characters
|
||||||
|
** and adds a wrapper similar to the following:
|
||||||
|
**
|
||||||
|
** replace(replace(<input>, '\n', char(10), '\r', char(13));
|
||||||
|
**
|
||||||
|
** Or, if the first character of the input is not "'", then a copy
|
||||||
|
** of the input is returned.
|
||||||
|
*/
|
||||||
|
static void shellEscapeCrnl(
|
||||||
|
sqlite3_context *context,
|
||||||
|
int argc,
|
||||||
|
sqlite3_value **argv
|
||||||
|
){
|
||||||
|
const char *zText = (const char*)sqlite3_value_text(argv[0]);
|
||||||
|
if( zText[0]=='\'' ){
|
||||||
|
int nText = sqlite3_value_bytes(argv[0]);
|
||||||
|
int i;
|
||||||
|
char zBuf1[20];
|
||||||
|
char zBuf2[20];
|
||||||
|
const char *zNL = 0;
|
||||||
|
const char *zCR = 0;
|
||||||
|
int nCR = 0;
|
||||||
|
int nNL = 0;
|
||||||
|
|
||||||
|
for(i=0; zText[i]; i++){
|
||||||
|
if( zNL==0 && zText[i]=='\n' ){
|
||||||
|
zNL = unused_string(zText, "\\n", "\\012", zBuf1);
|
||||||
|
nNL = (int)strlen(zNL);
|
||||||
|
}
|
||||||
|
if( zCR==0 && zText[i]=='\r' ){
|
||||||
|
zCR = unused_string(zText, "\\r", "\\015", zBuf2);
|
||||||
|
nCR = (int)strlen(zCR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( zNL || zCR ){
|
||||||
|
int iOut = 0;
|
||||||
|
i64 nMax = (nNL > nCR) ? nNL : nCR;
|
||||||
|
i64 nAlloc = nMax * nText + (nMax+12)*2;
|
||||||
|
char *zOut = (char*)sqlite3_malloc64(nAlloc);
|
||||||
|
if( zOut==0 ){
|
||||||
|
sqlite3_result_error_nomem(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( zNL && zCR ){
|
||||||
|
memcpy(&zOut[iOut], "replace(replace(", 16);
|
||||||
|
iOut += 16;
|
||||||
|
}else{
|
||||||
|
memcpy(&zOut[iOut], "replace(", 8);
|
||||||
|
iOut += 8;
|
||||||
|
}
|
||||||
|
for(i=0; zText[i]; i++){
|
||||||
|
if( zText[i]=='\n' ){
|
||||||
|
memcpy(&zOut[iOut], zNL, nNL);
|
||||||
|
iOut += nNL;
|
||||||
|
}else if( zText[i]=='\r' ){
|
||||||
|
memcpy(&zOut[iOut], zCR, nCR);
|
||||||
|
iOut += nCR;
|
||||||
|
}else{
|
||||||
|
zOut[iOut] = zText[i];
|
||||||
|
iOut++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( zNL ){
|
||||||
|
memcpy(&zOut[iOut], ",'", 2); iOut += 2;
|
||||||
|
memcpy(&zOut[iOut], zNL, nNL); iOut += nNL;
|
||||||
|
memcpy(&zOut[iOut], "', char(10))", 12); iOut += 12;
|
||||||
|
}
|
||||||
|
if( zCR ){
|
||||||
|
memcpy(&zOut[iOut], ",'", 2); iOut += 2;
|
||||||
|
memcpy(&zOut[iOut], zCR, nCR); iOut += nCR;
|
||||||
|
memcpy(&zOut[iOut], "', char(13))", 12); iOut += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_result_text(context, zOut, iOut, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_free(zOut);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_result_value(context, argv[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/* Flags for open_db().
|
/* Flags for open_db().
|
||||||
**
|
**
|
||||||
** The default behavior of open_db() is to exit(1) if the database fails to
|
** The default behavior of open_db() is to exit(1) if the database fails to
|
||||||
@ -3999,6 +4125,9 @@ static void open_db(ShellState *p, int openFlags){
|
|||||||
sqlite3_fileio_init(p->db, 0, 0);
|
sqlite3_fileio_init(p->db, 0, 0);
|
||||||
sqlite3_shathree_init(p->db, 0, 0);
|
sqlite3_shathree_init(p->db, 0, 0);
|
||||||
sqlite3_completion_init(p->db, 0, 0);
|
sqlite3_completion_init(p->db, 0, 0);
|
||||||
|
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||||
|
sqlite3_dbdata_init(p->db, 0, 0);
|
||||||
|
#endif
|
||||||
#ifdef SQLITE_HAVE_ZLIB
|
#ifdef SQLITE_HAVE_ZLIB
|
||||||
sqlite3_zipfile_init(p->db, 0, 0);
|
sqlite3_zipfile_init(p->db, 0, 0);
|
||||||
sqlite3_sqlar_init(p->db, 0, 0);
|
sqlite3_sqlar_init(p->db, 0, 0);
|
||||||
@ -4009,6 +4138,10 @@ static void open_db(ShellState *p, int openFlags){
|
|||||||
shellModuleSchema, 0, 0);
|
shellModuleSchema, 0, 0);
|
||||||
sqlite3_create_function(p->db, "shell_putsnl", 1, SQLITE_UTF8, p,
|
sqlite3_create_function(p->db, "shell_putsnl", 1, SQLITE_UTF8, p,
|
||||||
shellPutsFunc, 0, 0);
|
shellPutsFunc, 0, 0);
|
||||||
|
sqlite3_create_function(p->db, "shell_escape_crnl", 1, SQLITE_UTF8, 0,
|
||||||
|
shellEscapeCrnl, 0, 0);
|
||||||
|
sqlite3_create_function(p->db, "shell_int32", 2, SQLITE_UTF8, 0,
|
||||||
|
shellInt32, 0, 0);
|
||||||
#ifndef SQLITE_NOHAVE_SYSTEM
|
#ifndef SQLITE_NOHAVE_SYSTEM
|
||||||
sqlite3_create_function(p->db, "edit", 1, SQLITE_UTF8, 0,
|
sqlite3_create_function(p->db, "edit", 1, SQLITE_UTF8, 0,
|
||||||
editFunc, 0, 0);
|
editFunc, 0, 0);
|
||||||
@ -5263,10 +5396,7 @@ static int lintDotCommand(
|
|||||||
return SQLITE_ERROR;
|
return SQLITE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB)
|
#if !defined SQLITE_OMIT_VIRTUALTABLE
|
||||||
/*********************************************************************************
|
|
||||||
** The ".archive" or ".ar" command.
|
|
||||||
*/
|
|
||||||
static void shellPrepare(
|
static void shellPrepare(
|
||||||
sqlite3 *db,
|
sqlite3 *db,
|
||||||
int *pRc,
|
int *pRc,
|
||||||
@ -5337,6 +5467,12 @@ static void shellReset(
|
|||||||
*pRc = rc;
|
*pRc = rc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif /* !defined SQLITE_OMIT_VIRTUALTABLE */
|
||||||
|
|
||||||
|
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB)
|
||||||
|
/*********************************************************************************
|
||||||
|
** The ".archive" or ".ar" command.
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
** Structure representing a single ".ar" command.
|
** Structure representing a single ".ar" command.
|
||||||
*/
|
*/
|
||||||
@ -6026,6 +6162,631 @@ end_ar_command:
|
|||||||
**********************************************************************************/
|
**********************************************************************************/
|
||||||
#endif /* !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) */
|
#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);
|
||||||
|
}
|
||||||
|
*pRc = rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Like shellExec(), except that zFmt is a printf() style format string.
|
||||||
|
*/
|
||||||
|
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 ){
|
||||||
|
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_master 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pTab->zQuoted = shellMPrintf(&rc, "%Q", zName);
|
||||||
|
pTab->azlCol = (char**)shellMalloc(&rc, sizeof(char*) * (nSqlCol+1));
|
||||||
|
pTab->nCol = nSqlCol;
|
||||||
|
|
||||||
|
if( bIntkey ){
|
||||||
|
pTab->azlCol[0] = shellMPrintf(&rc, "%Q", zPk);
|
||||||
|
}else{
|
||||||
|
pTab->azlCol[0] = shellMPrintf(&rc, "");
|
||||||
|
}
|
||||||
|
i = 1;
|
||||||
|
shellPreparePrintf(dbtmp, &rc, &pStmt,
|
||||||
|
"SELECT %Q || group_concat(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 ){
|
||||||
|
recoverFreeTable(pTab);
|
||||||
|
pTab = 0;
|
||||||
|
}
|
||||||
|
return pTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This function is called to search the schema recovered from the
|
||||||
|
** sqlite_master 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);
|
||||||
|
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, "%Q", 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This function is called to recover data from the database. A script
|
||||||
|
** to construct a new database containing all recovered data is output
|
||||||
|
** on stream pState->out.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
int bFreelist = 1; /* 0 if --freelist-corrupt is specified */
|
||||||
|
for(i=1; i<nArg; i++){
|
||||||
|
char *z = azArg[i];
|
||||||
|
int n;
|
||||||
|
if( z[0]=='-' && z[1]=='-' ) z++;
|
||||||
|
n = strlen(z);
|
||||||
|
if( n<=17 && memcmp("-freelist-corrupt", z, n)==0 ){
|
||||||
|
bFreelist = 0;
|
||||||
|
}else
|
||||||
|
if( n<=12 && memcmp("-recovery-db", z, n)==0 && i<(nArg-1) ){
|
||||||
|
i++;
|
||||||
|
zRecoveryDb = azArg[i];
|
||||||
|
}else
|
||||||
|
if( n<=15 && memcmp("-lost-and-found", z, n)==0 && i<(nArg-1) ){
|
||||||
|
i++;
|
||||||
|
zLostAndFound = azArg[i];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
raw_printf(stderr, "unexpected option: %s\n", azArg[i]);
|
||||||
|
raw_printf(stderr, "options are:\n");
|
||||||
|
raw_printf(stderr, " --freelist-corrupt\n");
|
||||||
|
raw_printf(stderr, " --recovery-db DATABASE\n");
|
||||||
|
raw_printf(stderr, " --lost-and-found TABLE-NAME\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shellExecPrintf(pState->db, &rc,
|
||||||
|
/* Attach an in-memory database named 'recovery'. Create an indexed
|
||||||
|
** cache of the sqlite_dbptr virtual table. */
|
||||||
|
"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
|
||||||
|
);
|
||||||
|
|
||||||
|
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, 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;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
" 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 ALL"
|
||||||
|
" 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 > 0 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_master 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;
|
||||||
|
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 AND root NOT IN (SELECT rootpage FROM recovery.schema)"
|
||||||
|
, &pLoop
|
||||||
|
);
|
||||||
|
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
|
||||||
|
nOrphan = sqlite3_column_int(pLoop, 0);
|
||||||
|
}
|
||||||
|
shellFinalize(&rc, pLoop);
|
||||||
|
pLoop = 0;
|
||||||
|
pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan);
|
||||||
|
|
||||||
|
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(value)), ', ')"
|
||||||
|
"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;
|
||||||
|
|
||||||
|
pTab = recoverFindTable(pState, &rc, iRoot, bIntkey, nCol, &bNoop);
|
||||||
|
if( bNoop || rc ) continue;
|
||||||
|
if( pTab==0 ) pTab = pOrphan;
|
||||||
|
|
||||||
|
if( 0==sqlite3_stricmp(pTab->zQuoted, "'sqlite_sequence'") ){
|
||||||
|
raw_printf(pState->out, "DELETE FROM sqlite_sequence;\n");
|
||||||
|
}
|
||||||
|
sqlite3_bind_int(pPages, 1, iRoot);
|
||||||
|
sqlite3_bind_int(pCells, 2, pTab->iPk);
|
||||||
|
|
||||||
|
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPages) ){
|
||||||
|
int iPgno = sqlite3_column_int(pPages, 0);
|
||||||
|
sqlite3_bind_int(pCells, 1, iPgno);
|
||||||
|
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pCells) ){
|
||||||
|
int nField = sqlite3_column_int(pCells, 0);
|
||||||
|
const char *zVal = (const char*)sqlite3_column_text(pCells, 1);
|
||||||
|
|
||||||
|
nField = nField+1;
|
||||||
|
if( pTab==pOrphan ){
|
||||||
|
raw_printf(pState->out,
|
||||||
|
"INSERT INTO %s VALUES(%d, %d, %d, %s%s%s);\n",
|
||||||
|
pTab->zQuoted, iRoot, iPgno, nField,
|
||||||
|
bIntkey ? "" : "NULL, ", zVal, pTab->azlCol[nField]
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
raw_printf(pState->out, "INSERT INTO %s(%s) VALUES( %s );\n",
|
||||||
|
pTab->zQuoted, pTab->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_master 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);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** If an input line begins with "." then invoke this routine to
|
** If an input line begins with "." then invoke this routine to
|
||||||
@ -6313,6 +7074,13 @@ static int do_meta_command(char *zLine, ShellState *p){
|
|||||||
rc = shell_dbinfo_command(p, nArg, azArg);
|
rc = shell_dbinfo_command(p, nArg, azArg);
|
||||||
}else
|
}else
|
||||||
|
|
||||||
|
#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
|
||||||
|
if( c=='r' && strncmp(azArg[0], "recover", n)==0 ){
|
||||||
|
open_db(p, 0);
|
||||||
|
rc = recoverDatabaseCmd(p, nArg, azArg);
|
||||||
|
}else
|
||||||
|
#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
|
||||||
|
|
||||||
if( c=='d' && strncmp(azArg[0], "dump", n)==0 ){
|
if( c=='d' && strncmp(azArg[0], "dump", n)==0 ){
|
||||||
const char *zLike = 0;
|
const char *zLike = 0;
|
||||||
int i;
|
int i;
|
||||||
@ -6350,7 +7118,9 @@ static int do_meta_command(char *zLine, ShellState *p){
|
|||||||
zLike = azArg[i];
|
zLike = azArg[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open_db(p, 0);
|
open_db(p, 0);
|
||||||
|
|
||||||
/* When playing back a "dump", the content might appear in an order
|
/* When playing back a "dump", the content might appear in an order
|
||||||
** which causes immediate foreign key constraints to be violated.
|
** which causes immediate foreign key constraints to be violated.
|
||||||
** So disable foreign-key constraint enforcement to prevent problems. */
|
** So disable foreign-key constraint enforcement to prevent problems. */
|
||||||
@ -6398,7 +7168,7 @@ static int do_meta_command(char *zLine, ShellState *p){
|
|||||||
}
|
}
|
||||||
sqlite3_exec(p->db, "PRAGMA writable_schema=OFF;", 0, 0, 0);
|
sqlite3_exec(p->db, "PRAGMA writable_schema=OFF;", 0, 0, 0);
|
||||||
sqlite3_exec(p->db, "RELEASE dump;", 0, 0, 0);
|
sqlite3_exec(p->db, "RELEASE dump;", 0, 0, 0);
|
||||||
raw_printf(p->out, p->nErr ? "ROLLBACK; -- due to errors\n" : "COMMIT;\n");
|
raw_printf(p->out, p->nErr?"ROLLBACK; -- due to errors\n":"COMMIT;\n");
|
||||||
p->showHeader = savedShowHeader;
|
p->showHeader = savedShowHeader;
|
||||||
p->shellFlgs = savedShellFlags;
|
p->shellFlgs = savedShellFlags;
|
||||||
}else
|
}else
|
||||||
|
114
test/dbdata.test
Normal file
114
test/dbdata.test
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# 2019-04-11
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
#***********************************************************************
|
||||||
|
# This file implements regression tests for SQLite library. The
|
||||||
|
# focus of this file is testing the sqlite_dbpage virtual table.
|
||||||
|
#
|
||||||
|
|
||||||
|
set testdir [file dirname $argv0]
|
||||||
|
source $testdir/tester.tcl
|
||||||
|
set testprefix dbdata
|
||||||
|
|
||||||
|
ifcapable !vtab||!compound {
|
||||||
|
finish_test
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db enable_load_extension 1
|
||||||
|
if { [catch { db eval { SELECT load_extension('../dbdata') } }] } {
|
||||||
|
finish_test
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do_execsql_test 1.0 {
|
||||||
|
CREATE TABLE T1(a, b);
|
||||||
|
INSERT INTO t1(rowid, a ,b) VALUES(5, 'v', 'five');
|
||||||
|
INSERT INTO t1(rowid, a, b) VALUES(10, 'x', 'ten');
|
||||||
|
}
|
||||||
|
|
||||||
|
do_execsql_test 1.1 {
|
||||||
|
SELECT pgno, cell, field, quote(value) FROM sqlite_dbdata WHERE pgno=2;
|
||||||
|
} {
|
||||||
|
2 0 -1 5
|
||||||
|
2 0 0 'v'
|
||||||
|
2 0 1 'five'
|
||||||
|
2 1 -1 10
|
||||||
|
2 1 0 'x'
|
||||||
|
2 1 1 'ten'
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpoint
|
||||||
|
do_execsql_test 1.2 {
|
||||||
|
SELECT pgno, cell, field, quote(value) FROM sqlite_dbdata;
|
||||||
|
} {
|
||||||
|
1 0 -1 1
|
||||||
|
1 0 0 'table'
|
||||||
|
1 0 1 'T1'
|
||||||
|
1 0 2 'T1'
|
||||||
|
1 0 3 2
|
||||||
|
1 0 4 {'CREATE TABLE T1(a, b)'}
|
||||||
|
2 0 -1 5
|
||||||
|
2 0 0 'v'
|
||||||
|
2 0 1 'five'
|
||||||
|
2 1 -1 10
|
||||||
|
2 1 0 'x'
|
||||||
|
2 1 1 'ten'
|
||||||
|
}
|
||||||
|
|
||||||
|
set big [string repeat big 2000]
|
||||||
|
do_execsql_test 1.3 {
|
||||||
|
INSERT INTO t1 VALUES(NULL, $big);
|
||||||
|
SELECT value FROM sqlite_dbdata WHERE pgno=2 AND cell=2 AND field=1;
|
||||||
|
} $big
|
||||||
|
|
||||||
|
do_execsql_test 1.4 {
|
||||||
|
DELETE FROM t1;
|
||||||
|
INSERT INTO t1 VALUES(NULL, randomblob(5050));
|
||||||
|
}
|
||||||
|
do_test 1.5 {
|
||||||
|
execsql {
|
||||||
|
SELECT quote(value) FROM sqlite_dbdata WHERE pgno=2 AND cell=0 AND field=1;
|
||||||
|
}
|
||||||
|
} [db one {SELECT quote(b) FROM t1}]
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
reset_db
|
||||||
|
db enable_load_extension 1
|
||||||
|
db eval { SELECT load_extension('../dbdata') }
|
||||||
|
|
||||||
|
do_execsql_test 2.0 {
|
||||||
|
CREATE TABLE t1(a);
|
||||||
|
CREATE INDEX i1 ON t1(a);
|
||||||
|
WITH s(i) AS (
|
||||||
|
SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10
|
||||||
|
)
|
||||||
|
INSERT INTO t1 SELECT randomblob(900) FROM s;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_execsql_test 2.1 {
|
||||||
|
SELECT * FROM sqlite_dbptr WHERE pgno=2;
|
||||||
|
} {
|
||||||
|
2 25 2 6 2 7 2 9 2 11 2 13 2 15 2 17 2 19 2 21
|
||||||
|
}
|
||||||
|
|
||||||
|
do_execsql_test 2.2 {
|
||||||
|
SELECT * FROM sqlite_dbptr WHERE pgno=3;
|
||||||
|
} {
|
||||||
|
3 24 3 23
|
||||||
|
}
|
||||||
|
|
||||||
|
do_execsql_test 2.3 {
|
||||||
|
SELECT * FROM sqlite_dbptr
|
||||||
|
} {
|
||||||
|
2 25 2 6 2 7 2 9 2 11 2 13 2 15 2 17 2 19 2 21
|
||||||
|
3 24 3 23
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
finish_test
|
129
test/recover.test
Normal file
129
test/recover.test
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 2019 April 23
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
#***********************************************************************
|
||||||
|
#
|
||||||
|
# Test the shell tool ".ar" command.
|
||||||
|
#
|
||||||
|
|
||||||
|
set testdir [file dirname $argv0]
|
||||||
|
source $testdir/tester.tcl
|
||||||
|
set testprefix recover
|
||||||
|
|
||||||
|
ifcapable !vtab {
|
||||||
|
finish_test; return
|
||||||
|
}
|
||||||
|
set CLI [test_find_cli]
|
||||||
|
|
||||||
|
proc compare_result {db1 db2 sql} {
|
||||||
|
set r1 [$db1 eval $sql]
|
||||||
|
set r2 [$db2 eval $sql]
|
||||||
|
if {$r1 != $r2} {
|
||||||
|
puts "r1: $r1"
|
||||||
|
puts "r2: $r2"
|
||||||
|
error "mismatch for $sql"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
proc compare_dbs {db1 db2} {
|
||||||
|
compare_result $db1 $db2 "SELECT sql FROM sqlite_master ORDER BY 1"
|
||||||
|
foreach tbl [$db1 eval {SELECT name FROM sqlite_master WHERE type='table'}] {
|
||||||
|
compare_result $db1 $db2 "SELECT * FROM $tbl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc do_recover_test {tn {tsql {}} {res {}}} {
|
||||||
|
set fd [open "|$::CLI test.db .recover"]
|
||||||
|
fconfigure $fd -encoding binary
|
||||||
|
fconfigure $fd -translation binary
|
||||||
|
set sql [read $fd]
|
||||||
|
close $fd
|
||||||
|
|
||||||
|
forcedelete test.db2
|
||||||
|
sqlite3 db2 test.db2
|
||||||
|
execsql $sql db2
|
||||||
|
if {$tsql==""} {
|
||||||
|
uplevel [list do_test $tn [list compare_dbs db db2] {}]
|
||||||
|
} else {
|
||||||
|
uplevel [list do_execsql_test -db db2 $tn $tsql $res]
|
||||||
|
}
|
||||||
|
db2 close
|
||||||
|
}
|
||||||
|
|
||||||
|
set doc {
|
||||||
|
hello
|
||||||
|
world
|
||||||
|
}
|
||||||
|
do_execsql_test 1.1.1 {
|
||||||
|
CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
|
||||||
|
INSERT INTO t1 VALUES(1, 4, X'1234567800');
|
||||||
|
INSERT INTO t1 VALUES(2, 'test', 8.1);
|
||||||
|
INSERT INTO t1 VALUES(3, $doc, 8.4);
|
||||||
|
}
|
||||||
|
do_recover_test 1.1.2
|
||||||
|
|
||||||
|
do_execsql_test 1.2.1 "
|
||||||
|
DELETE FROM t1;
|
||||||
|
INSERT INTO t1 VALUES(13, 'hello\r\nworld', 13);
|
||||||
|
"
|
||||||
|
do_recover_test 1.2.2
|
||||||
|
|
||||||
|
do_execsql_test 1.3.1 "
|
||||||
|
CREATE TABLE t2(i INTEGER PRIMARY KEY AUTOINCREMENT, b, c);
|
||||||
|
INSERT INTO t2 VALUES(NULL, 1, 2);
|
||||||
|
INSERT INTO t2 VALUES(NULL, 3, 4);
|
||||||
|
INSERT INTO t2 VALUES(NULL, 5, 6);
|
||||||
|
CREATE TABLE t3(i INTEGER PRIMARY KEY AUTOINCREMENT, b, c);
|
||||||
|
INSERT INTO t3 VALUES(NULL, 1, 2);
|
||||||
|
INSERT INTO t3 VALUES(NULL, 3, 4);
|
||||||
|
INSERT INTO t3 VALUES(NULL, 5, 6);
|
||||||
|
DELETE FROM t2;
|
||||||
|
"
|
||||||
|
do_recover_test 1.3.2
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
reset_db
|
||||||
|
do_execsql_test 2.1.0 {
|
||||||
|
CREATE TABLE t1(a, b, c, PRIMARY KEY(b, c)) WITHOUT ROWID;
|
||||||
|
INSERT INTO t1 VALUES(1, 2, 3);
|
||||||
|
INSERT INTO t1 VALUES(4, 5, 6);
|
||||||
|
INSERT INTO t1 VALUES(7, 8, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_recover_test 2.1.1
|
||||||
|
|
||||||
|
do_execsql_test 2.2.0 {
|
||||||
|
PRAGMA writable_schema = 1;
|
||||||
|
DELETE FROM sqlite_master WHERE name='t1';
|
||||||
|
}
|
||||||
|
do_recover_test 2.2.1 {
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
} {lost_and_found}
|
||||||
|
|
||||||
|
do_execsql_test 2.3.0 {
|
||||||
|
CREATE TABLE lost_and_found(a, b, c);
|
||||||
|
}
|
||||||
|
do_recover_test 2.3.1 {
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
} {lost_and_found lost_and_found_0}
|
||||||
|
|
||||||
|
do_execsql_test 2.4.0 {
|
||||||
|
CREATE TABLE lost_and_found_0(a, b, c);
|
||||||
|
}
|
||||||
|
do_recover_test 2.4.1 {
|
||||||
|
SELECT name FROM sqlite_master;
|
||||||
|
SELECT * FROM lost_and_found_1;
|
||||||
|
} {lost_and_found lost_and_found_0 lost_and_found_1
|
||||||
|
2 2 3 {} 2 3 1
|
||||||
|
2 2 3 {} 5 6 4
|
||||||
|
2 2 3 {} 8 9 7
|
||||||
|
}
|
||||||
|
|
||||||
|
finish_test
|
@ -40,16 +40,21 @@ proc omit_redundant_typedefs {line} {
|
|||||||
}
|
}
|
||||||
return $line
|
return $line
|
||||||
}
|
}
|
||||||
|
set iLine 0
|
||||||
while {1} {
|
while {1} {
|
||||||
set lx [omit_redundant_typedefs [gets $in]]
|
set lx [omit_redundant_typedefs [gets $in]]
|
||||||
if {[eof $in]} break;
|
if {[eof $in]} break;
|
||||||
|
incr iLine
|
||||||
if {[regexp {^INCLUDE } $lx]} {
|
if {[regexp {^INCLUDE } $lx]} {
|
||||||
set cfile [lindex $lx 1]
|
set cfile [lindex $lx 1]
|
||||||
puts $out "/************************* Begin $cfile ******************/"
|
puts $out "/************************* Begin $cfile ******************/"
|
||||||
|
# puts $out "#line 1 \"$cfile\""
|
||||||
set in2 [open $topdir/src/$cfile rb]
|
set in2 [open $topdir/src/$cfile rb]
|
||||||
while {![eof $in2]} {
|
while {![eof $in2]} {
|
||||||
set lx [omit_redundant_typedefs [gets $in2]]
|
set lx [omit_redundant_typedefs [gets $in2]]
|
||||||
if {[regexp {^#include "sqlite} $lx]} continue
|
if {[regexp {^#include "sqlite} $lx]} {
|
||||||
|
set lx "/* $lx */"
|
||||||
|
}
|
||||||
if {[regexp {^# *include "test_windirent.h"} $lx]} {
|
if {[regexp {^# *include "test_windirent.h"} $lx]} {
|
||||||
set lx "/* $lx */"
|
set lx "/* $lx */"
|
||||||
}
|
}
|
||||||
@ -58,6 +63,7 @@ while {1} {
|
|||||||
}
|
}
|
||||||
close $in2
|
close $in2
|
||||||
puts $out "/************************* End $cfile ********************/"
|
puts $out "/************************* End $cfile ********************/"
|
||||||
|
# puts $out "#line [expr $iLine+1] \"shell.c.in\""
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
puts $out $lx
|
puts $out $lx
|
||||||
|
Loading…
Reference in New Issue
Block a user