You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
CLI v1.0.168
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
This folder contains extensions and utility programs intended to analyze
|
||||
live database files, detect problems, and possibly fix them.
|
||||
|
||||
As SQLite is being used on larger and larger databases, database sizes
|
||||
are growing into the terabyte range. At that size, hardware malfunctions
|
||||
and/or cosmic rays will occasionally corrupt a database file. Detecting
|
||||
problems and fixing errors a terabyte-sized databases can take hours or days,
|
||||
and it is undesirable to take applications that depend on the databases
|
||||
off-line for such a long time.
|
||||
The utilities in the folder are intended to provide mechanisms for
|
||||
detecting and fixing problems in large databases while those databases
|
||||
are in active use.
|
||||
|
||||
The utilities and extensions in this folder are experimental and under
|
||||
active development at the time of this writing (2017-10-12). If and when
|
||||
they stabilize, this README will be updated to reflect that fact.
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
** 2017 October 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 module exports a single C function:
|
||||
**
|
||||
** int sqlite3_check_freelist(sqlite3 *db, const char *zDb);
|
||||
**
|
||||
** This function checks the free-list in database zDb (one of "main",
|
||||
** "temp", etc.) and reports any errors by invoking the sqlite3_log()
|
||||
** function. It returns SQLITE_OK if successful, or an SQLite error
|
||||
** code otherwise. It is not an error if the free-list is corrupted but
|
||||
** no IO or OOM errors occur.
|
||||
**
|
||||
** If this file is compiled and loaded as an SQLite loadable extension,
|
||||
** it adds an SQL function "checkfreelist" to the database handle, to
|
||||
** be invoked as follows:
|
||||
**
|
||||
** SELECT checkfreelist(<database-name>);
|
||||
**
|
||||
** This function performs the same checks as sqlite3_check_freelist(),
|
||||
** except that it returns all error messages as a single text value,
|
||||
** separated by newline characters. If the freelist is not corrupted
|
||||
** in any way, an empty string is returned.
|
||||
**
|
||||
** To compile this module for use as an SQLite loadable extension:
|
||||
**
|
||||
** gcc -Os -fPIC -shared checkfreelist.c -o checkfreelist.so
|
||||
*/
|
||||
|
||||
#include "sqlite3ext.h"
|
||||
SQLITE_EXTENSION_INIT1
|
||||
|
||||
#ifndef SQLITE_AMALGAMATION
|
||||
# include <string.h>
|
||||
# include <stdio.h>
|
||||
# include <stdlib.h>
|
||||
# include <assert.h>
|
||||
# define ALWAYS(X) 1
|
||||
# define NEVER(X) 0
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned short u16;
|
||||
typedef unsigned int u32;
|
||||
#define get4byte(x) ( \
|
||||
((u32)((x)[0])<<24) + \
|
||||
((u32)((x)[1])<<16) + \
|
||||
((u32)((x)[2])<<8) + \
|
||||
((u32)((x)[3])) \
|
||||
)
|
||||
#endif
|
||||
|
||||
/*
|
||||
** Execute a single PRAGMA statement and return the integer value returned
|
||||
** via output parameter (*pnOut).
|
||||
**
|
||||
** The SQL statement passed as the third argument should be a printf-style
|
||||
** format string containing a single "%s" which will be replace by the
|
||||
** value passed as the second argument. e.g.
|
||||
**
|
||||
** sqlGetInteger(db, "main", "PRAGMA %s.page_count", pnOut)
|
||||
**
|
||||
** executes "PRAGMA main.page_count" and stores the results in (*pnOut).
|
||||
*/
|
||||
static int sqlGetInteger(
|
||||
sqlite3 *db, /* Database handle */
|
||||
const char *zDb, /* Database name ("main", "temp" etc.) */
|
||||
const char *zFmt, /* SQL statement format */
|
||||
u32 *pnOut /* OUT: Integer value */
|
||||
){
|
||||
int rc, rc2;
|
||||
char *zSql;
|
||||
sqlite3_stmt *pStmt = 0;
|
||||
int bOk = 0;
|
||||
|
||||
zSql = sqlite3_mprintf(zFmt, zDb);
|
||||
if( zSql==0 ){
|
||||
rc = SQLITE_NOMEM;
|
||||
}else{
|
||||
rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
|
||||
sqlite3_free(zSql);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
|
||||
*pnOut = (u32)sqlite3_column_int(pStmt, 0);
|
||||
bOk = 1;
|
||||
}
|
||||
|
||||
rc2 = sqlite3_finalize(pStmt);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
if( rc==SQLITE_OK && bOk==0 ) rc = SQLITE_ERROR;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Argument zFmt must be a printf-style format string and must be
|
||||
** followed by its required arguments. If argument pzOut is NULL,
|
||||
** then the results of printf()ing the format string are passed to
|
||||
** sqlite3_log(). Otherwise, they are appended to the string
|
||||
** at (*pzOut).
|
||||
*/
|
||||
static int checkFreelistError(char **pzOut, const char *zFmt, ...){
|
||||
int rc = SQLITE_OK;
|
||||
char *zErr = 0;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, zFmt);
|
||||
zErr = sqlite3_vmprintf(zFmt, ap);
|
||||
if( zErr==0 ){
|
||||
rc = SQLITE_NOMEM;
|
||||
}else{
|
||||
if( pzOut ){
|
||||
*pzOut = sqlite3_mprintf("%s%z%s", *pzOut?"\n":"", *pzOut, zErr);
|
||||
if( *pzOut==0 ) rc = SQLITE_NOMEM;
|
||||
}else{
|
||||
sqlite3_log(SQLITE_ERROR, "checkfreelist: %s", zErr);
|
||||
}
|
||||
sqlite3_free(zErr);
|
||||
}
|
||||
va_end(ap);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int checkFreelist(
|
||||
sqlite3 *db,
|
||||
const char *zDb,
|
||||
char **pzOut
|
||||
){
|
||||
/* This query returns one row for each page on the free list. Each row has
|
||||
** two columns - the page number and page content. */
|
||||
const char *zTrunk =
|
||||
"WITH freelist_trunk(i, d, n) AS ("
|
||||
"SELECT 1, NULL, sqlite_readint32(data, 32) "
|
||||
"FROM sqlite_dbpage(:1) WHERE pgno=1 "
|
||||
"UNION ALL "
|
||||
"SELECT n, data, sqlite_readint32(data) "
|
||||
"FROM freelist_trunk, sqlite_dbpage(:1) WHERE pgno=n "
|
||||
")"
|
||||
"SELECT i, d FROM freelist_trunk WHERE i!=1;";
|
||||
|
||||
int rc, rc2; /* Return code */
|
||||
sqlite3_stmt *pTrunk = 0; /* Compilation of zTrunk */
|
||||
u32 nPage = 0; /* Number of pages in db */
|
||||
u32 nExpected = 0; /* Expected number of free pages */
|
||||
u32 nFree = 0; /* Number of pages on free list */
|
||||
|
||||
if( zDb==0 ) zDb = "main";
|
||||
|
||||
if( (rc = sqlGetInteger(db, zDb, "PRAGMA %s.page_count", &nPage))
|
||||
|| (rc = sqlGetInteger(db, zDb, "PRAGMA %s.freelist_count", &nExpected))
|
||||
){
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = sqlite3_prepare_v2(db, zTrunk, -1, &pTrunk, 0);
|
||||
if( rc!=SQLITE_OK ) return rc;
|
||||
sqlite3_bind_text(pTrunk, 1, zDb, -1, SQLITE_STATIC);
|
||||
while( rc==SQLITE_OK && sqlite3_step(pTrunk)==SQLITE_ROW ){
|
||||
u32 i;
|
||||
u32 iTrunk = (u32)sqlite3_column_int(pTrunk, 0);
|
||||
const u8 *aData = (const u8*)sqlite3_column_blob(pTrunk, 1);
|
||||
u32 nData = (u32)sqlite3_column_bytes(pTrunk, 1);
|
||||
u32 iNext = get4byte(&aData[0]);
|
||||
u32 nLeaf = get4byte(&aData[4]);
|
||||
|
||||
if( nLeaf>((nData/4)-2-6) ){
|
||||
rc = checkFreelistError(pzOut,
|
||||
"leaf count out of range (%d) on trunk page %d",
|
||||
(int)nLeaf, (int)iTrunk
|
||||
);
|
||||
nLeaf = (nData/4) - 2 - 6;
|
||||
}
|
||||
|
||||
nFree += 1+nLeaf;
|
||||
if( iNext>nPage ){
|
||||
rc = checkFreelistError(pzOut,
|
||||
"trunk page %d is out of range", (int)iNext
|
||||
);
|
||||
}
|
||||
|
||||
for(i=0; rc==SQLITE_OK && i<nLeaf; i++){
|
||||
u32 iLeaf = get4byte(&aData[8 + 4*i]);
|
||||
if( iLeaf==0 || iLeaf>nPage ){
|
||||
rc = checkFreelistError(pzOut,
|
||||
"leaf page %d is out of range (child %d of trunk page %d)",
|
||||
(int)iLeaf, (int)i, (int)iTrunk
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && nFree!=nExpected ){
|
||||
rc = checkFreelistError(pzOut,
|
||||
"free-list count mismatch: actual=%d header=%d",
|
||||
(int)nFree, (int)nExpected
|
||||
);
|
||||
}
|
||||
|
||||
rc2 = sqlite3_finalize(pTrunk);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int sqlite3_check_freelist(sqlite3 *db, const char *zDb){
|
||||
return checkFreelist(db, zDb, 0);
|
||||
}
|
||||
|
||||
static void checkfreelist_function(
|
||||
sqlite3_context *pCtx,
|
||||
int nArg,
|
||||
sqlite3_value **apArg
|
||||
){
|
||||
const char *zDb;
|
||||
int rc;
|
||||
char *zOut = 0;
|
||||
sqlite3 *db = sqlite3_context_db_handle(pCtx);
|
||||
|
||||
assert( nArg==1 );
|
||||
zDb = (const char*)sqlite3_value_text(apArg[0]);
|
||||
rc = checkFreelist(db, zDb, &zOut);
|
||||
if( rc==SQLITE_OK ){
|
||||
sqlite3_result_text(pCtx, zOut?zOut:"ok", -1, SQLITE_TRANSIENT);
|
||||
}else{
|
||||
sqlite3_result_error_code(pCtx, rc);
|
||||
}
|
||||
|
||||
sqlite3_free(zOut);
|
||||
}
|
||||
|
||||
/*
|
||||
** An SQL function invoked as follows:
|
||||
**
|
||||
** sqlite_readint32(BLOB) -- Decode 32-bit integer from start of blob
|
||||
*/
|
||||
static void readint_function(
|
||||
sqlite3_context *pCtx,
|
||||
int nArg,
|
||||
sqlite3_value **apArg
|
||||
){
|
||||
const u8 *zBlob;
|
||||
int nBlob;
|
||||
int iOff = 0;
|
||||
u32 iRet = 0;
|
||||
|
||||
if( nArg!=1 && nArg!=2 ){
|
||||
sqlite3_result_error(
|
||||
pCtx, "wrong number of arguments to function sqlite_readint32()", -1
|
||||
);
|
||||
return;
|
||||
}
|
||||
if( nArg==2 ){
|
||||
iOff = sqlite3_value_int(apArg[1]);
|
||||
}
|
||||
|
||||
zBlob = sqlite3_value_blob(apArg[0]);
|
||||
nBlob = sqlite3_value_bytes(apArg[0]);
|
||||
|
||||
if( nBlob>=(iOff+4) ){
|
||||
iRet = get4byte(&zBlob[iOff]);
|
||||
}
|
||||
|
||||
sqlite3_result_int64(pCtx, (sqlite3_int64)iRet);
|
||||
}
|
||||
|
||||
/*
|
||||
** Register the SQL functions.
|
||||
*/
|
||||
static int cflRegister(sqlite3 *db){
|
||||
int rc = sqlite3_create_function(
|
||||
db, "sqlite_readint32", -1, SQLITE_UTF8, 0, readint_function, 0, 0
|
||||
);
|
||||
if( rc!=SQLITE_OK ) return rc;
|
||||
rc = sqlite3_create_function(
|
||||
db, "checkfreelist", 1, SQLITE_UTF8, 0, checkfreelist_function, 0, 0
|
||||
);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Extension load function.
|
||||
*/
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
int sqlite3_checkfreelist_init(
|
||||
sqlite3 *db,
|
||||
char **pzErrMsg,
|
||||
const sqlite3_api_routines *pApi
|
||||
){
|
||||
SQLITE_EXTENSION_INIT2(pApi);
|
||||
return cflRegister(db);
|
||||
}
|
||||
@@ -0,0 +1,927 @@
|
||||
/*
|
||||
** 2017 October 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 "sqlite3ext.h"
|
||||
SQLITE_EXTENSION_INIT1
|
||||
|
||||
/*
|
||||
** Stuff that is available inside the amalgamation, but which we need to
|
||||
** declare ourselves if this module is compiled separately.
|
||||
*/
|
||||
#ifndef SQLITE_AMALGAMATION
|
||||
# include <string.h>
|
||||
# include <stdio.h>
|
||||
# include <stdlib.h>
|
||||
# include <assert.h>
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned short u16;
|
||||
typedef unsigned int u32;
|
||||
#define get4byte(x) ( \
|
||||
((u32)((x)[0])<<24) + \
|
||||
((u32)((x)[1])<<16) + \
|
||||
((u32)((x)[2])<<8) + \
|
||||
((u32)((x)[3])) \
|
||||
)
|
||||
#endif
|
||||
|
||||
typedef struct CidxTable CidxTable;
|
||||
typedef struct CidxCursor CidxCursor;
|
||||
|
||||
struct CidxTable {
|
||||
sqlite3_vtab base; /* Base class. Must be first */
|
||||
sqlite3 *db;
|
||||
};
|
||||
|
||||
struct CidxCursor {
|
||||
sqlite3_vtab_cursor base; /* Base class. Must be first */
|
||||
sqlite3_int64 iRowid; /* Row number of the output */
|
||||
char *zIdxName; /* Copy of the index_name parameter */
|
||||
char *zAfterKey; /* Copy of the after_key parameter */
|
||||
sqlite3_stmt *pStmt; /* SQL statement that generates the output */
|
||||
};
|
||||
|
||||
typedef struct CidxColumn CidxColumn;
|
||||
struct CidxColumn {
|
||||
char *zExpr; /* Text for indexed expression */
|
||||
int bDesc; /* True for DESC columns, otherwise false */
|
||||
int bKey; /* Part of index, not PK */
|
||||
};
|
||||
|
||||
typedef struct CidxIndex CidxIndex;
|
||||
struct CidxIndex {
|
||||
char *zWhere; /* WHERE clause, if any */
|
||||
int nCol; /* Elements in aCol[] array */
|
||||
CidxColumn aCol[1]; /* Array of indexed columns */
|
||||
};
|
||||
|
||||
static void *cidxMalloc(int *pRc, int n){
|
||||
void *pRet = 0;
|
||||
assert( n!=0 );
|
||||
if( *pRc==SQLITE_OK ){
|
||||
pRet = sqlite3_malloc(n);
|
||||
if( pRet ){
|
||||
memset(pRet, 0, n);
|
||||
}else{
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}
|
||||
}
|
||||
return pRet;
|
||||
}
|
||||
|
||||
static void cidxCursorError(CidxCursor *pCsr, const char *zFmt, ...){
|
||||
va_list ap;
|
||||
va_start(ap, zFmt);
|
||||
assert( pCsr->base.pVtab->zErrMsg==0 );
|
||||
pCsr->base.pVtab->zErrMsg = sqlite3_vmprintf(zFmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
/*
|
||||
** Connect to the incremental_index_check virtual table.
|
||||
*/
|
||||
static int cidxConnect(
|
||||
sqlite3 *db,
|
||||
void *pAux,
|
||||
int argc, const char *const*argv,
|
||||
sqlite3_vtab **ppVtab,
|
||||
char **pzErr
|
||||
){
|
||||
int rc = SQLITE_OK;
|
||||
CidxTable *pRet;
|
||||
|
||||
#define IIC_ERRMSG 0
|
||||
#define IIC_CURRENT_KEY 1
|
||||
#define IIC_INDEX_NAME 2
|
||||
#define IIC_AFTER_KEY 3
|
||||
#define IIC_SCANNER_SQL 4
|
||||
rc = sqlite3_declare_vtab(db,
|
||||
"CREATE TABLE xyz("
|
||||
" errmsg TEXT," /* Error message or NULL if everything is ok */
|
||||
" current_key TEXT," /* SQLite quote() text of key values */
|
||||
" index_name HIDDEN," /* IN: name of the index being scanned */
|
||||
" after_key HIDDEN," /* IN: Start scanning after this key */
|
||||
" scanner_sql HIDDEN" /* debuggingn info: SQL used for scanner */
|
||||
")"
|
||||
);
|
||||
pRet = cidxMalloc(&rc, sizeof(CidxTable));
|
||||
if( pRet ){
|
||||
pRet->db = db;
|
||||
}
|
||||
|
||||
*ppVtab = (sqlite3_vtab*)pRet;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Disconnect from or destroy an incremental_index_check virtual table.
|
||||
*/
|
||||
static int cidxDisconnect(sqlite3_vtab *pVtab){
|
||||
CidxTable *pTab = (CidxTable*)pVtab;
|
||||
sqlite3_free(pTab);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** idxNum and idxStr are not used. There are only three possible plans,
|
||||
** which are all distinguished by the number of parameters.
|
||||
**
|
||||
** No parameters: A degenerate plan. The result is zero rows.
|
||||
** 1 Parameter: Scan all of the index starting with first entry
|
||||
** 2 parameters: Scan the index starting after the "after_key".
|
||||
**
|
||||
** Provide successively smaller costs for each of these plans to encourage
|
||||
** the query planner to select the one with the most parameters.
|
||||
*/
|
||||
static int cidxBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pInfo){
|
||||
int iIdxName = -1;
|
||||
int iAfterKey = -1;
|
||||
int i;
|
||||
|
||||
for(i=0; i<pInfo->nConstraint; i++){
|
||||
struct sqlite3_index_constraint *p = &pInfo->aConstraint[i];
|
||||
if( p->usable==0 ) continue;
|
||||
if( p->op!=SQLITE_INDEX_CONSTRAINT_EQ ) continue;
|
||||
|
||||
if( p->iColumn==IIC_INDEX_NAME ){
|
||||
iIdxName = i;
|
||||
}
|
||||
if( p->iColumn==IIC_AFTER_KEY ){
|
||||
iAfterKey = i;
|
||||
}
|
||||
}
|
||||
|
||||
if( iIdxName<0 ){
|
||||
pInfo->estimatedCost = 1000000000.0;
|
||||
}else{
|
||||
pInfo->aConstraintUsage[iIdxName].argvIndex = 1;
|
||||
pInfo->aConstraintUsage[iIdxName].omit = 1;
|
||||
if( iAfterKey<0 ){
|
||||
pInfo->estimatedCost = 1000000.0;
|
||||
}else{
|
||||
pInfo->aConstraintUsage[iAfterKey].argvIndex = 2;
|
||||
pInfo->aConstraintUsage[iAfterKey].omit = 1;
|
||||
pInfo->estimatedCost = 1000.0;
|
||||
}
|
||||
}
|
||||
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** Open a new btreeinfo cursor.
|
||||
*/
|
||||
static int cidxOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){
|
||||
CidxCursor *pRet;
|
||||
int rc = SQLITE_OK;
|
||||
|
||||
pRet = cidxMalloc(&rc, sizeof(CidxCursor));
|
||||
|
||||
*ppCursor = (sqlite3_vtab_cursor*)pRet;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Close a btreeinfo cursor.
|
||||
*/
|
||||
static int cidxClose(sqlite3_vtab_cursor *pCursor){
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
sqlite3_finalize(pCsr->pStmt);
|
||||
sqlite3_free(pCsr->zIdxName);
|
||||
sqlite3_free(pCsr->zAfterKey);
|
||||
sqlite3_free(pCsr);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** Move a btreeinfo cursor to the next entry in the file.
|
||||
*/
|
||||
static int cidxNext(sqlite3_vtab_cursor *pCursor){
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
int rc = sqlite3_step(pCsr->pStmt);
|
||||
if( rc!=SQLITE_ROW ){
|
||||
rc = sqlite3_finalize(pCsr->pStmt);
|
||||
pCsr->pStmt = 0;
|
||||
if( rc!=SQLITE_OK ){
|
||||
sqlite3 *db = ((CidxTable*)pCsr->base.pVtab)->db;
|
||||
cidxCursorError(pCsr, "Cursor error: %s", sqlite3_errmsg(db));
|
||||
}
|
||||
}else{
|
||||
pCsr->iRowid++;
|
||||
rc = SQLITE_OK;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* We have reached EOF if previous sqlite3_step() returned
|
||||
** anything other than SQLITE_ROW;
|
||||
*/
|
||||
static int cidxEof(sqlite3_vtab_cursor *pCursor){
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
return pCsr->pStmt==0;
|
||||
}
|
||||
|
||||
static char *cidxMprintf(int *pRc, const char *zFmt, ...){
|
||||
char *zRet = 0;
|
||||
va_list ap;
|
||||
va_start(ap, zFmt);
|
||||
zRet = sqlite3_vmprintf(zFmt, ap);
|
||||
if( *pRc==SQLITE_OK ){
|
||||
if( zRet==0 ){
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}
|
||||
}else{
|
||||
sqlite3_free(zRet);
|
||||
zRet = 0;
|
||||
}
|
||||
va_end(ap);
|
||||
return zRet;
|
||||
}
|
||||
|
||||
static sqlite3_stmt *cidxPrepare(
|
||||
int *pRc, CidxCursor *pCsr, const char *zFmt, ...
|
||||
){
|
||||
sqlite3_stmt *pRet = 0;
|
||||
char *zSql;
|
||||
va_list ap; /* ... printf arguments */
|
||||
va_start(ap, zFmt);
|
||||
|
||||
zSql = sqlite3_vmprintf(zFmt, ap);
|
||||
if( *pRc==SQLITE_OK ){
|
||||
if( zSql==0 ){
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}else{
|
||||
sqlite3 *db = ((CidxTable*)pCsr->base.pVtab)->db;
|
||||
*pRc = sqlite3_prepare_v2(db, zSql, -1, &pRet, 0);
|
||||
if( *pRc!=SQLITE_OK ){
|
||||
cidxCursorError(pCsr, "SQL error: %s", sqlite3_errmsg(db));
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_free(zSql);
|
||||
va_end(ap);
|
||||
|
||||
return pRet;
|
||||
}
|
||||
|
||||
static void cidxFinalize(int *pRc, sqlite3_stmt *pStmt){
|
||||
int rc = sqlite3_finalize(pStmt);
|
||||
if( *pRc==SQLITE_OK ) *pRc = rc;
|
||||
}
|
||||
|
||||
char *cidxStrdup(int *pRc, const char *zStr){
|
||||
char *zRet = 0;
|
||||
if( *pRc==SQLITE_OK ){
|
||||
int n = (int)strlen(zStr);
|
||||
zRet = cidxMalloc(pRc, n+1);
|
||||
if( zRet ) memcpy(zRet, zStr, n+1);
|
||||
}
|
||||
return zRet;
|
||||
}
|
||||
|
||||
static void cidxFreeIndex(CidxIndex *pIdx){
|
||||
if( pIdx ){
|
||||
int i;
|
||||
for(i=0; i<pIdx->nCol; i++){
|
||||
sqlite3_free(pIdx->aCol[i].zExpr);
|
||||
}
|
||||
sqlite3_free(pIdx->zWhere);
|
||||
sqlite3_free(pIdx);
|
||||
}
|
||||
}
|
||||
|
||||
static int cidx_isspace(char c){
|
||||
return c==' ' || c=='\t' || c=='\r' || c=='\n';
|
||||
}
|
||||
|
||||
static int cidx_isident(char c){
|
||||
return c<0
|
||||
|| (c>='0' && c<='9') || (c>='a' && c<='z')
|
||||
|| (c>='A' && c<='Z') || c=='_';
|
||||
}
|
||||
|
||||
#define CIDX_PARSE_EOF 0
|
||||
#define CIDX_PARSE_COMMA 1 /* "," */
|
||||
#define CIDX_PARSE_OPEN 2 /* "(" */
|
||||
#define CIDX_PARSE_CLOSE 3 /* ")" */
|
||||
|
||||
/*
|
||||
** Argument zIn points into the start, middle or end of a CREATE INDEX
|
||||
** statement. If argument pbDoNotTrim is non-NULL, then this function
|
||||
** scans the input until it finds EOF, a comma (",") or an open or
|
||||
** close parenthesis character. It then sets (*pzOut) to point to said
|
||||
** character and returns a CIDX_PARSE_XXX constant as appropriate. The
|
||||
** parser is smart enough that special characters inside SQL strings
|
||||
** or comments are not returned for.
|
||||
**
|
||||
** Or, if argument pbDoNotTrim is NULL, then this function sets *pzOut
|
||||
** to point to the first character of the string that is not whitespace
|
||||
** or part of an SQL comment and returns CIDX_PARSE_EOF.
|
||||
**
|
||||
** Additionally, if pbDoNotTrim is not NULL and the element immediately
|
||||
** before (*pzOut) is an SQL comment of the form "-- comment", then
|
||||
** (*pbDoNotTrim) is set before returning. In all other cases it is
|
||||
** cleared.
|
||||
*/
|
||||
static int cidxFindNext(
|
||||
const char *zIn,
|
||||
const char **pzOut,
|
||||
int *pbDoNotTrim /* OUT: True if prev is -- comment */
|
||||
){
|
||||
const char *z = zIn;
|
||||
|
||||
while( 1 ){
|
||||
while( cidx_isspace(*z) ) z++;
|
||||
if( z[0]=='-' && z[1]=='-' ){
|
||||
z += 2;
|
||||
while( z[0]!='\n' ){
|
||||
if( z[0]=='\0' ) return CIDX_PARSE_EOF;
|
||||
z++;
|
||||
}
|
||||
while( cidx_isspace(*z) ) z++;
|
||||
if( pbDoNotTrim ) *pbDoNotTrim = 1;
|
||||
}else
|
||||
if( z[0]=='/' && z[1]=='*' ){
|
||||
z += 2;
|
||||
while( z[0]!='*' || z[1]!='/' ){
|
||||
if( z[1]=='\0' ) return CIDX_PARSE_EOF;
|
||||
z++;
|
||||
}
|
||||
z += 2;
|
||||
}else{
|
||||
*pzOut = z;
|
||||
if( pbDoNotTrim==0 ) return CIDX_PARSE_EOF;
|
||||
switch( *z ){
|
||||
case '\0':
|
||||
return CIDX_PARSE_EOF;
|
||||
case '(':
|
||||
return CIDX_PARSE_OPEN;
|
||||
case ')':
|
||||
return CIDX_PARSE_CLOSE;
|
||||
case ',':
|
||||
return CIDX_PARSE_COMMA;
|
||||
|
||||
case '"':
|
||||
case '\'':
|
||||
case '`': {
|
||||
char q = *z;
|
||||
z++;
|
||||
while( *z ){
|
||||
if( *z==q ){
|
||||
z++;
|
||||
if( *z!=q ) break;
|
||||
}
|
||||
z++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case '[':
|
||||
while( *z++!=']' );
|
||||
break;
|
||||
|
||||
default:
|
||||
z++;
|
||||
break;
|
||||
}
|
||||
*pbDoNotTrim = 0;
|
||||
}
|
||||
}
|
||||
|
||||
assert( 0 );
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int cidxParseSQL(CidxCursor *pCsr, CidxIndex *pIdx, const char *zSql){
|
||||
const char *z = zSql;
|
||||
const char *z1;
|
||||
int e;
|
||||
int rc = SQLITE_OK;
|
||||
int nParen = 1;
|
||||
int bDoNotTrim = 0;
|
||||
CidxColumn *pCol = pIdx->aCol;
|
||||
|
||||
e = cidxFindNext(z, &z, &bDoNotTrim);
|
||||
if( e!=CIDX_PARSE_OPEN ) goto parse_error;
|
||||
z1 = z+1;
|
||||
z++;
|
||||
while( nParen>0 ){
|
||||
e = cidxFindNext(z, &z, &bDoNotTrim);
|
||||
if( e==CIDX_PARSE_EOF ) goto parse_error;
|
||||
if( (e==CIDX_PARSE_COMMA || e==CIDX_PARSE_CLOSE) && nParen==1 ){
|
||||
const char *z2 = z;
|
||||
if( pCol->zExpr ) goto parse_error;
|
||||
|
||||
if( bDoNotTrim==0 ){
|
||||
while( cidx_isspace(z[-1]) ) z--;
|
||||
if( !sqlite3_strnicmp(&z[-3], "asc", 3) && 0==cidx_isident(z[-4]) ){
|
||||
z -= 3;
|
||||
while( cidx_isspace(z[-1]) ) z--;
|
||||
}else
|
||||
if( !sqlite3_strnicmp(&z[-4], "desc", 4) && 0==cidx_isident(z[-5]) ){
|
||||
z -= 4;
|
||||
while( cidx_isspace(z[-1]) ) z--;
|
||||
}
|
||||
while( cidx_isspace(z1[0]) ) z1++;
|
||||
}
|
||||
|
||||
pCol->zExpr = cidxMprintf(&rc, "%.*s", z-z1, z1);
|
||||
pCol++;
|
||||
z = z1 = z2+1;
|
||||
}
|
||||
if( e==CIDX_PARSE_OPEN ) nParen++;
|
||||
if( e==CIDX_PARSE_CLOSE ) nParen--;
|
||||
z++;
|
||||
}
|
||||
|
||||
/* Search for a WHERE clause */
|
||||
cidxFindNext(z, &z, 0);
|
||||
if( 0==sqlite3_strnicmp(z, "where", 5) ){
|
||||
pIdx->zWhere = cidxMprintf(&rc, "%s\n", &z[5]);
|
||||
}else if( z[0]!='\0' ){
|
||||
goto parse_error;
|
||||
}
|
||||
|
||||
return rc;
|
||||
|
||||
parse_error:
|
||||
cidxCursorError(pCsr, "Parse error in: %s", zSql);
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
static int cidxLookupIndex(
|
||||
CidxCursor *pCsr, /* Cursor object */
|
||||
const char *zIdx, /* Name of index to look up */
|
||||
CidxIndex **ppIdx, /* OUT: Description of columns */
|
||||
char **pzTab /* OUT: Table name */
|
||||
){
|
||||
int rc = SQLITE_OK;
|
||||
char *zTab = 0;
|
||||
CidxIndex *pIdx = 0;
|
||||
|
||||
sqlite3_stmt *pFindTab = 0;
|
||||
sqlite3_stmt *pInfo = 0;
|
||||
|
||||
/* Find the table for this index. */
|
||||
pFindTab = cidxPrepare(&rc, pCsr,
|
||||
"SELECT tbl_name, sql FROM sqlite_schema WHERE name=%Q AND type='index'",
|
||||
zIdx
|
||||
);
|
||||
if( rc==SQLITE_OK && sqlite3_step(pFindTab)==SQLITE_ROW ){
|
||||
const char *zSql = (const char*)sqlite3_column_text(pFindTab, 1);
|
||||
zTab = cidxStrdup(&rc, (const char*)sqlite3_column_text(pFindTab, 0));
|
||||
|
||||
pInfo = cidxPrepare(&rc, pCsr, "PRAGMA index_xinfo(%Q)", zIdx);
|
||||
if( rc==SQLITE_OK ){
|
||||
int nAlloc = 0;
|
||||
int iCol = 0;
|
||||
|
||||
while( sqlite3_step(pInfo)==SQLITE_ROW ){
|
||||
const char *zName = (const char*)sqlite3_column_text(pInfo, 2);
|
||||
const char *zColl = (const char*)sqlite3_column_text(pInfo, 4);
|
||||
CidxColumn *p;
|
||||
if( zName==0 ) zName = "rowid";
|
||||
if( iCol==nAlloc ){
|
||||
int nByte = sizeof(CidxIndex) + sizeof(CidxColumn)*(nAlloc+8);
|
||||
pIdx = (CidxIndex*)sqlite3_realloc(pIdx, nByte);
|
||||
nAlloc += 8;
|
||||
}
|
||||
p = &pIdx->aCol[iCol++];
|
||||
p->bDesc = sqlite3_column_int(pInfo, 3);
|
||||
p->bKey = sqlite3_column_int(pInfo, 5);
|
||||
if( zSql==0 || p->bKey==0 ){
|
||||
p->zExpr = cidxMprintf(&rc, "\"%w\" COLLATE %s",zName,zColl);
|
||||
}else{
|
||||
p->zExpr = 0;
|
||||
}
|
||||
pIdx->nCol = iCol;
|
||||
pIdx->zWhere = 0;
|
||||
}
|
||||
cidxFinalize(&rc, pInfo);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && zSql ){
|
||||
rc = cidxParseSQL(pCsr, pIdx, zSql);
|
||||
}
|
||||
}
|
||||
|
||||
cidxFinalize(&rc, pFindTab);
|
||||
if( rc==SQLITE_OK && zTab==0 ){
|
||||
rc = SQLITE_ERROR;
|
||||
}
|
||||
|
||||
if( rc!=SQLITE_OK ){
|
||||
sqlite3_free(zTab);
|
||||
cidxFreeIndex(pIdx);
|
||||
}else{
|
||||
*pzTab = zTab;
|
||||
*ppIdx = pIdx;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int cidxDecodeAfter(
|
||||
CidxCursor *pCsr,
|
||||
int nCol,
|
||||
const char *zAfterKey,
|
||||
char ***pazAfter
|
||||
){
|
||||
char **azAfter;
|
||||
int rc = SQLITE_OK;
|
||||
int nAfterKey = (int)strlen(zAfterKey);
|
||||
|
||||
azAfter = cidxMalloc(&rc, sizeof(char*)*nCol + nAfterKey+1);
|
||||
if( rc==SQLITE_OK ){
|
||||
int i;
|
||||
char *zCopy = (char*)&azAfter[nCol];
|
||||
char *p = zCopy;
|
||||
memcpy(zCopy, zAfterKey, nAfterKey+1);
|
||||
for(i=0; i<nCol; i++){
|
||||
while( *p==' ' ) p++;
|
||||
|
||||
/* Check NULL values */
|
||||
if( *p=='N' ){
|
||||
if( memcmp(p, "NULL", 4) ) goto parse_error;
|
||||
p += 4;
|
||||
}
|
||||
|
||||
/* Check strings and blob literals */
|
||||
else if( *p=='X' || *p=='\'' ){
|
||||
azAfter[i] = p;
|
||||
if( *p=='X' ) p++;
|
||||
if( *p!='\'' ) goto parse_error;
|
||||
p++;
|
||||
while( 1 ){
|
||||
if( *p=='\0' ) goto parse_error;
|
||||
if( *p=='\'' ){
|
||||
p++;
|
||||
if( *p!='\'' ) break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check numbers */
|
||||
else{
|
||||
azAfter[i] = p;
|
||||
while( (*p>='0' && *p<='9')
|
||||
|| *p=='.' || *p=='+' || *p=='-' || *p=='e' || *p=='E'
|
||||
){
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
while( *p==' ' ) p++;
|
||||
if( *p!=(i==(nCol-1) ? '\0' : ',') ){
|
||||
goto parse_error;
|
||||
}
|
||||
*p++ = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
*pazAfter = azAfter;
|
||||
return rc;
|
||||
|
||||
parse_error:
|
||||
sqlite3_free(azAfter);
|
||||
*pazAfter = 0;
|
||||
cidxCursorError(pCsr, "%s", "error parsing after value");
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
static char *cidxWhere(
|
||||
int *pRc, CidxColumn *aCol, char **azAfter, int iGt, int bLastIsNull
|
||||
){
|
||||
char *zRet = 0;
|
||||
const char *zSep = "";
|
||||
int i;
|
||||
|
||||
for(i=0; i<iGt; i++){
|
||||
zRet = cidxMprintf(pRc, "%z%s(%s) IS %s", zRet,
|
||||
zSep, aCol[i].zExpr, (azAfter[i] ? azAfter[i] : "NULL")
|
||||
);
|
||||
zSep = " AND ";
|
||||
}
|
||||
|
||||
if( bLastIsNull ){
|
||||
zRet = cidxMprintf(pRc, "%z%s(%s) IS NULL", zRet, zSep, aCol[iGt].zExpr);
|
||||
}
|
||||
else if( azAfter[iGt] ){
|
||||
zRet = cidxMprintf(pRc, "%z%s(%s) %s %s", zRet,
|
||||
zSep, aCol[iGt].zExpr, (aCol[iGt].bDesc ? "<" : ">"),
|
||||
azAfter[iGt]
|
||||
);
|
||||
}else{
|
||||
zRet = cidxMprintf(pRc, "%z%s(%s) IS NOT NULL", zRet, zSep,aCol[iGt].zExpr);
|
||||
}
|
||||
|
||||
return zRet;
|
||||
}
|
||||
|
||||
#define CIDX_CLIST_ALL 0
|
||||
#define CIDX_CLIST_ORDERBY 1
|
||||
#define CIDX_CLIST_CURRENT_KEY 2
|
||||
#define CIDX_CLIST_SUBWHERE 3
|
||||
#define CIDX_CLIST_SUBEXPR 4
|
||||
|
||||
/*
|
||||
** This function returns various strings based on the contents of the
|
||||
** CidxIndex structure and the eType parameter.
|
||||
*/
|
||||
static char *cidxColumnList(
|
||||
int *pRc, /* IN/OUT: Error code */
|
||||
const char *zIdx,
|
||||
CidxIndex *pIdx, /* Indexed columns */
|
||||
int eType /* True to include ASC/DESC */
|
||||
){
|
||||
char *zRet = 0;
|
||||
if( *pRc==SQLITE_OK ){
|
||||
const char *aDir[2] = {"", " DESC"};
|
||||
int i;
|
||||
const char *zSep = "";
|
||||
|
||||
for(i=0; i<pIdx->nCol; i++){
|
||||
CidxColumn *p = &pIdx->aCol[i];
|
||||
assert( pIdx->aCol[i].bDesc==0 || pIdx->aCol[i].bDesc==1 );
|
||||
switch( eType ){
|
||||
|
||||
case CIDX_CLIST_ORDERBY:
|
||||
zRet = cidxMprintf(pRc, "%z%s%d%s", zRet, zSep, i+1, aDir[p->bDesc]);
|
||||
zSep = ",";
|
||||
break;
|
||||
|
||||
case CIDX_CLIST_CURRENT_KEY:
|
||||
zRet = cidxMprintf(pRc, "%z%squote(i%d)", zRet, zSep, i);
|
||||
zSep = "||','||";
|
||||
break;
|
||||
|
||||
case CIDX_CLIST_SUBWHERE:
|
||||
if( p->bKey==0 ){
|
||||
zRet = cidxMprintf(pRc, "%z%s%s IS i.i%d", zRet,
|
||||
zSep, p->zExpr, i
|
||||
);
|
||||
zSep = " AND ";
|
||||
}
|
||||
break;
|
||||
|
||||
case CIDX_CLIST_SUBEXPR:
|
||||
if( p->bKey==1 ){
|
||||
zRet = cidxMprintf(pRc, "%z%s%s IS i.i%d", zRet,
|
||||
zSep, p->zExpr, i
|
||||
);
|
||||
zSep = " AND ";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
assert( eType==CIDX_CLIST_ALL );
|
||||
zRet = cidxMprintf(pRc, "%z%s(%s) AS i%d", zRet, zSep, p->zExpr, i);
|
||||
zSep = ", ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zRet;
|
||||
}
|
||||
|
||||
/*
|
||||
** Generate SQL (in memory obtained from sqlite3_malloc()) that will
|
||||
** continue the index scan for zIdxName starting after zAfterKey.
|
||||
*/
|
||||
int cidxGenerateScanSql(
|
||||
CidxCursor *pCsr, /* The cursor which needs the new statement */
|
||||
const char *zIdxName, /* index to be scanned */
|
||||
const char *zAfterKey, /* start after this key, if not NULL */
|
||||
char **pzSqlOut /* OUT: Write the generated SQL here */
|
||||
){
|
||||
int rc;
|
||||
char *zTab = 0;
|
||||
char *zCurrentKey = 0;
|
||||
char *zOrderBy = 0;
|
||||
char *zSubWhere = 0;
|
||||
char *zSubExpr = 0;
|
||||
char *zSrcList = 0;
|
||||
char **azAfter = 0;
|
||||
CidxIndex *pIdx = 0;
|
||||
|
||||
*pzSqlOut = 0;
|
||||
rc = cidxLookupIndex(pCsr, zIdxName, &pIdx, &zTab);
|
||||
|
||||
zOrderBy = cidxColumnList(&rc, zIdxName, pIdx, CIDX_CLIST_ORDERBY);
|
||||
zCurrentKey = cidxColumnList(&rc, zIdxName, pIdx, CIDX_CLIST_CURRENT_KEY);
|
||||
zSubWhere = cidxColumnList(&rc, zIdxName, pIdx, CIDX_CLIST_SUBWHERE);
|
||||
zSubExpr = cidxColumnList(&rc, zIdxName, pIdx, CIDX_CLIST_SUBEXPR);
|
||||
zSrcList = cidxColumnList(&rc, zIdxName, pIdx, CIDX_CLIST_ALL);
|
||||
|
||||
if( rc==SQLITE_OK && zAfterKey ){
|
||||
rc = cidxDecodeAfter(pCsr, pIdx->nCol, zAfterKey, &azAfter);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
if( zAfterKey==0 ){
|
||||
*pzSqlOut = cidxMprintf(&rc,
|
||||
"SELECT (SELECT %s FROM %Q AS t WHERE %s), %s "
|
||||
"FROM (SELECT %s FROM %Q INDEXED BY %Q %s%sORDER BY %s) AS i",
|
||||
zSubExpr, zTab, zSubWhere, zCurrentKey,
|
||||
zSrcList, zTab, zIdxName,
|
||||
(pIdx->zWhere ? "WHERE " : ""), (pIdx->zWhere ? pIdx->zWhere : ""),
|
||||
zOrderBy
|
||||
);
|
||||
}else{
|
||||
const char *zSep = "";
|
||||
char *zSql;
|
||||
int i;
|
||||
|
||||
zSql = cidxMprintf(&rc,
|
||||
"SELECT (SELECT %s FROM %Q WHERE %s), %s FROM (",
|
||||
zSubExpr, zTab, zSubWhere, zCurrentKey
|
||||
);
|
||||
for(i=pIdx->nCol-1; i>=0; i--){
|
||||
int j;
|
||||
if( pIdx->aCol[i].bDesc && azAfter[i]==0 ) continue;
|
||||
for(j=0; j<2; j++){
|
||||
char *zWhere = cidxWhere(&rc, pIdx->aCol, azAfter, i, j);
|
||||
zSql = cidxMprintf(&rc, "%z"
|
||||
"%sSELECT * FROM ("
|
||||
"SELECT %s FROM %Q INDEXED BY %Q WHERE %s%s%z ORDER BY %s"
|
||||
")",
|
||||
zSql, zSep, zSrcList, zTab, zIdxName,
|
||||
pIdx->zWhere ? pIdx->zWhere : "",
|
||||
pIdx->zWhere ? " AND " : "",
|
||||
zWhere, zOrderBy
|
||||
);
|
||||
zSep = " UNION ALL ";
|
||||
if( pIdx->aCol[i].bDesc==0 ) break;
|
||||
}
|
||||
}
|
||||
*pzSqlOut = cidxMprintf(&rc, "%z) AS i", zSql);
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_free(zTab);
|
||||
sqlite3_free(zCurrentKey);
|
||||
sqlite3_free(zOrderBy);
|
||||
sqlite3_free(zSubWhere);
|
||||
sqlite3_free(zSubExpr);
|
||||
sqlite3_free(zSrcList);
|
||||
cidxFreeIndex(pIdx);
|
||||
sqlite3_free(azAfter);
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** Position a cursor back to the beginning.
|
||||
*/
|
||||
static int cidxFilter(
|
||||
sqlite3_vtab_cursor *pCursor,
|
||||
int idxNum, const char *idxStr,
|
||||
int argc, sqlite3_value **argv
|
||||
){
|
||||
int rc = SQLITE_OK;
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
const char *zIdxName = 0;
|
||||
const char *zAfterKey = 0;
|
||||
|
||||
sqlite3_free(pCsr->zIdxName);
|
||||
pCsr->zIdxName = 0;
|
||||
sqlite3_free(pCsr->zAfterKey);
|
||||
pCsr->zAfterKey = 0;
|
||||
sqlite3_finalize(pCsr->pStmt);
|
||||
pCsr->pStmt = 0;
|
||||
|
||||
if( argc>0 ){
|
||||
zIdxName = (const char*)sqlite3_value_text(argv[0]);
|
||||
if( argc>1 ){
|
||||
zAfterKey = (const char*)sqlite3_value_text(argv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if( zIdxName ){
|
||||
char *zSql = 0;
|
||||
pCsr->zIdxName = sqlite3_mprintf("%s", zIdxName);
|
||||
pCsr->zAfterKey = zAfterKey ? sqlite3_mprintf("%s", zAfterKey) : 0;
|
||||
rc = cidxGenerateScanSql(pCsr, zIdxName, zAfterKey, &zSql);
|
||||
if( zSql ){
|
||||
pCsr->pStmt = cidxPrepare(&rc, pCsr, "%z", zSql);
|
||||
}
|
||||
}
|
||||
|
||||
if( pCsr->pStmt ){
|
||||
assert( rc==SQLITE_OK );
|
||||
rc = cidxNext(pCursor);
|
||||
}
|
||||
pCsr->iRowid = 1;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Return a column value.
|
||||
*/
|
||||
static int cidxColumn(
|
||||
sqlite3_vtab_cursor *pCursor,
|
||||
sqlite3_context *ctx,
|
||||
int iCol
|
||||
){
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
assert( iCol>=IIC_ERRMSG && iCol<=IIC_SCANNER_SQL );
|
||||
switch( iCol ){
|
||||
case IIC_ERRMSG: {
|
||||
const char *zVal = 0;
|
||||
if( sqlite3_column_type(pCsr->pStmt, 0)==SQLITE_INTEGER ){
|
||||
if( sqlite3_column_int(pCsr->pStmt, 0)==0 ){
|
||||
zVal = "row data mismatch";
|
||||
}
|
||||
}else{
|
||||
zVal = "row missing";
|
||||
}
|
||||
sqlite3_result_text(ctx, zVal, -1, SQLITE_STATIC);
|
||||
break;
|
||||
}
|
||||
case IIC_CURRENT_KEY: {
|
||||
sqlite3_result_value(ctx, sqlite3_column_value(pCsr->pStmt, 1));
|
||||
break;
|
||||
}
|
||||
case IIC_INDEX_NAME: {
|
||||
sqlite3_result_text(ctx, pCsr->zIdxName, -1, SQLITE_TRANSIENT);
|
||||
break;
|
||||
}
|
||||
case IIC_AFTER_KEY: {
|
||||
sqlite3_result_text(ctx, pCsr->zAfterKey, -1, SQLITE_TRANSIENT);
|
||||
break;
|
||||
}
|
||||
case IIC_SCANNER_SQL: {
|
||||
char *zSql = 0;
|
||||
cidxGenerateScanSql(pCsr, pCsr->zIdxName, pCsr->zAfterKey, &zSql);
|
||||
sqlite3_result_text(ctx, zSql, -1, sqlite3_free);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/* Return the ROWID for the sqlite_btreeinfo table */
|
||||
static int cidxRowid(sqlite3_vtab_cursor *pCursor, sqlite_int64 *pRowid){
|
||||
CidxCursor *pCsr = (CidxCursor*)pCursor;
|
||||
*pRowid = pCsr->iRowid;
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** Register the virtual table modules with the database handle passed
|
||||
** as the only argument.
|
||||
*/
|
||||
static int ciInit(sqlite3 *db){
|
||||
static sqlite3_module cidx_module = {
|
||||
0, /* iVersion */
|
||||
0, /* xCreate */
|
||||
cidxConnect, /* xConnect */
|
||||
cidxBestIndex, /* xBestIndex */
|
||||
cidxDisconnect, /* xDisconnect */
|
||||
0, /* xDestroy */
|
||||
cidxOpen, /* xOpen - open a cursor */
|
||||
cidxClose, /* xClose - close a cursor */
|
||||
cidxFilter, /* xFilter - configure scan constraints */
|
||||
cidxNext, /* xNext - advance a cursor */
|
||||
cidxEof, /* xEof - check for end of scan */
|
||||
cidxColumn, /* xColumn - read data */
|
||||
cidxRowid, /* xRowid - read data */
|
||||
0, /* xUpdate */
|
||||
0, /* xBegin */
|
||||
0, /* xSync */
|
||||
0, /* xCommit */
|
||||
0, /* xRollback */
|
||||
0, /* xFindMethod */
|
||||
0, /* xRename */
|
||||
0, /* xSavepoint */
|
||||
0, /* xRelease */
|
||||
0, /* xRollbackTo */
|
||||
};
|
||||
return sqlite3_create_module(db, "incremental_index_check", &cidx_module, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
** Extension load function.
|
||||
*/
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
int sqlite3_checkindex_init(
|
||||
sqlite3 *db,
|
||||
char **pzErrMsg,
|
||||
const sqlite3_api_routines *pApi
|
||||
){
|
||||
SQLITE_EXTENSION_INIT2(pApi);
|
||||
return ciInit(db);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
** Read an SQLite database file and analyze its space utilization. Generate
|
||||
** text on standard output.
|
||||
*/
|
||||
#define TCLSH_INIT_PROC sqlite3_checker_init_proc
|
||||
#define SQLITE_ENABLE_DBPAGE_VTAB 1
|
||||
#define SQLITE_ENABLE_JSON1 1
|
||||
#undef SQLITE_THREADSAFE
|
||||
#define SQLITE_THREADSAFE 0
|
||||
#undef SQLITE_ENABLE_COLUMN_METADATA
|
||||
#define SQLITE_OMIT_DECLTYPE 1
|
||||
#define SQLITE_OMIT_DEPRECATED 1
|
||||
#define SQLITE_OMIT_PROGRESS_CALLBACK 1
|
||||
#define SQLITE_OMIT_SHARED_CACHE 1
|
||||
#define SQLITE_DEFAULT_MEMSTATUS 0
|
||||
#define SQLITE_MAX_EXPR_DEPTH 0
|
||||
INCLUDE sqlite3.c
|
||||
INCLUDE $ROOT/src/tclsqlite.c
|
||||
INCLUDE $ROOT/ext/misc/btreeinfo.c
|
||||
INCLUDE $ROOT/ext/repair/checkindex.c
|
||||
INCLUDE $ROOT/ext/repair/checkfreelist.c
|
||||
|
||||
/*
|
||||
** Decode a pointer to an sqlite3 object.
|
||||
*/
|
||||
int getDbPointer(Tcl_Interp *interp, const char *zA, sqlite3 **ppDb){
|
||||
struct SqliteDb *p;
|
||||
Tcl_CmdInfo cmdInfo;
|
||||
if( Tcl_GetCommandInfo(interp, zA, &cmdInfo) ){
|
||||
p = (struct SqliteDb*)cmdInfo.objClientData;
|
||||
*ppDb = p->db;
|
||||
return TCL_OK;
|
||||
}else{
|
||||
*ppDb = 0;
|
||||
return TCL_ERROR;
|
||||
}
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** sqlite3_imposter db main rootpage {CREATE TABLE...} ;# setup an imposter
|
||||
** sqlite3_imposter db main ;# rm all imposters
|
||||
*/
|
||||
static int sqlite3_imposter(
|
||||
void *clientData,
|
||||
Tcl_Interp *interp,
|
||||
int objc,
|
||||
Tcl_Obj *CONST objv[]
|
||||
){
|
||||
sqlite3 *db;
|
||||
const char *zSchema;
|
||||
int iRoot;
|
||||
const char *zSql;
|
||||
|
||||
if( objc!=3 && objc!=5 ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv, "DB SCHEMA [ROOTPAGE SQL]");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ) return TCL_ERROR;
|
||||
zSchema = Tcl_GetString(objv[2]);
|
||||
if( objc==3 ){
|
||||
sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, zSchema, 0, 1);
|
||||
}else{
|
||||
if( Tcl_GetIntFromObj(interp, objv[3], &iRoot) ) return TCL_ERROR;
|
||||
zSql = Tcl_GetString(objv[4]);
|
||||
sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, zSchema, 1, iRoot);
|
||||
sqlite3_exec(db, zSql, 0, 0, 0);
|
||||
sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, zSchema, 0, 0);
|
||||
}
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
const char *sqlite3_checker_init_proc(Tcl_Interp *interp){
|
||||
Tcl_CreateObjCommand(interp, "sqlite3_imposter",
|
||||
(Tcl_ObjCmdProc*)sqlite3_imposter, 0, 0);
|
||||
sqlite3_auto_extension((void(*)(void))sqlite3_btreeinfo_init);
|
||||
sqlite3_auto_extension((void(*)(void))sqlite3_checkindex_init);
|
||||
sqlite3_auto_extension((void(*)(void))sqlite3_checkfreelist_init);
|
||||
return
|
||||
BEGIN_STRING
|
||||
INCLUDE $ROOT/ext/repair/sqlite3_checker.tcl
|
||||
END_STRING
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
# This TCL script is the main driver script for the sqlite3_checker utility
|
||||
# program.
|
||||
#
|
||||
|
||||
# Special case:
|
||||
#
|
||||
# sqlite3_checker --test FILENAME ARGS
|
||||
#
|
||||
# uses FILENAME in place of this script.
|
||||
#
|
||||
if {[lindex $argv 0]=="--test" && [llength $argv]>1} {
|
||||
set ::argv0 [lindex $argv 1]
|
||||
set argv [lrange $argv 2 end]
|
||||
source $argv0
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Emulate a TCL shell
|
||||
#
|
||||
proc tclsh {} {
|
||||
set line {}
|
||||
while {![eof stdin]} {
|
||||
if {$line!=""} {
|
||||
puts -nonewline "> "
|
||||
} else {
|
||||
puts -nonewline "% "
|
||||
}
|
||||
flush stdout
|
||||
append line [gets stdin]
|
||||
if {[info complete $line]} {
|
||||
if {[catch {uplevel #0 $line} result]} {
|
||||
puts stderr "Error: $result"
|
||||
} elseif {$result!=""} {
|
||||
puts $result
|
||||
}
|
||||
set line {}
|
||||
} else {
|
||||
append line \n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Do an incremental integrity check of a single index
|
||||
#
|
||||
proc check_index {idxname batchsize bTrace} {
|
||||
set i 0
|
||||
set more 1
|
||||
set nerr 0
|
||||
set pct 00.0
|
||||
set max [db one {SELECT nEntry FROM sqlite_btreeinfo('main')
|
||||
WHERE name=$idxname}]
|
||||
puts -nonewline "$idxname: $i of $max rows ($pct%)\r"
|
||||
flush stdout
|
||||
if {$bTrace} {
|
||||
set sql {SELECT errmsg, current_key AS key,
|
||||
CASE WHEN rowid=1 THEN scanner_sql END AS traceOut
|
||||
FROM incremental_index_check($idxname)
|
||||
WHERE after_key=$key
|
||||
LIMIT $batchsize}
|
||||
} else {
|
||||
set sql {SELECT errmsg, current_key AS key, NULL AS traceOut
|
||||
FROM incremental_index_check($idxname)
|
||||
WHERE after_key=$key
|
||||
LIMIT $batchsize}
|
||||
}
|
||||
while {$more} {
|
||||
set more 0
|
||||
db eval $sql {
|
||||
set more 1
|
||||
if {$errmsg!=""} {
|
||||
incr nerr
|
||||
puts "$idxname: key($key): $errmsg"
|
||||
} elseif {$traceOut!=""} {
|
||||
puts "$idxname: $traceOut"
|
||||
}
|
||||
incr i
|
||||
|
||||
}
|
||||
set x [format {%.1f} [expr {($i*100.0)/$max}]]
|
||||
if {$x!=$pct} {
|
||||
puts -nonewline "$idxname: $i of $max rows ($pct%)\r"
|
||||
flush stdout
|
||||
set pct $x
|
||||
}
|
||||
}
|
||||
puts "$idxname: $nerr errors out of $i entries"
|
||||
}
|
||||
|
||||
# Print a usage message on standard error, then quit.
|
||||
#
|
||||
proc usage {} {
|
||||
set argv0 [file rootname [file tail [info nameofexecutable]]]
|
||||
puts stderr "Usage: $argv0 OPTIONS database-filename"
|
||||
puts stderr {
|
||||
Do sanity checking on a live SQLite3 database file specified by the
|
||||
"database-filename" argument.
|
||||
|
||||
Options:
|
||||
|
||||
--batchsize N Number of rows to check per transaction
|
||||
|
||||
--freelist Perform a freelist check
|
||||
|
||||
--index NAME Run a check of the index NAME
|
||||
|
||||
--summary Print summary information about the database
|
||||
|
||||
--table NAME Run a check of all indexes for table NAME
|
||||
|
||||
--tclsh Run the built-in TCL interpreter (for debugging)
|
||||
|
||||
--trace (Debugging only:) Output trace information on the scan
|
||||
|
||||
--version Show the version number of SQLite
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
set file_to_analyze {}
|
||||
append argv {}
|
||||
set bFreelistCheck 0
|
||||
set bSummary 0
|
||||
set zIndex {}
|
||||
set zTable {}
|
||||
set batchsize 1000
|
||||
set bAll 1
|
||||
set bTrace 0
|
||||
set argc [llength $argv]
|
||||
for {set i 0} {$i<$argc} {incr i} {
|
||||
set arg [lindex $argv $i]
|
||||
if {[regexp {^-+tclsh$} $arg]} {
|
||||
tclsh
|
||||
exit 0
|
||||
}
|
||||
if {[regexp {^-+version$} $arg]} {
|
||||
sqlite3 mem :memory:
|
||||
puts [mem one {SELECT sqlite_version()||' '||sqlite_source_id()}]
|
||||
mem close
|
||||
exit 0
|
||||
}
|
||||
if {[regexp {^-+freelist$} $arg]} {
|
||||
set bFreelistCheck 1
|
||||
set bAll 0
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-+summary$} $arg]} {
|
||||
set bSummary 1
|
||||
set bAll 0
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-+trace$} $arg]} {
|
||||
set bTrace 1
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-+batchsize$} $arg]} {
|
||||
incr i
|
||||
if {$i>=$argc} {
|
||||
puts stderr "missing argument on $arg"
|
||||
exit 1
|
||||
}
|
||||
set batchsize [lindex $argv $i]
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-+index$} $arg]} {
|
||||
incr i
|
||||
if {$i>=$argc} {
|
||||
puts stderr "missing argument on $arg"
|
||||
exit 1
|
||||
}
|
||||
set zIndex [lindex $argv $i]
|
||||
set bAll 0
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-+table$} $arg]} {
|
||||
incr i
|
||||
if {$i>=$argc} {
|
||||
puts stderr "missing argument on $arg"
|
||||
exit 1
|
||||
}
|
||||
set zTable [lindex $argv $i]
|
||||
set bAll 0
|
||||
continue
|
||||
}
|
||||
if {[regexp {^-} $arg]} {
|
||||
puts stderr "Unknown option: $arg"
|
||||
usage
|
||||
}
|
||||
if {$file_to_analyze!=""} {
|
||||
usage
|
||||
} else {
|
||||
set file_to_analyze $arg
|
||||
}
|
||||
}
|
||||
if {$file_to_analyze==""} usage
|
||||
|
||||
# If a TCL script is specified on the command-line, then run that
|
||||
# script.
|
||||
#
|
||||
if {[file extension $file_to_analyze]==".tcl"} {
|
||||
source $file_to_analyze
|
||||
exit 0
|
||||
}
|
||||
|
||||
set root_filename $file_to_analyze
|
||||
regexp {^file:(//)?([^?]*)} $file_to_analyze all x1 root_filename
|
||||
if {![file exists $root_filename]} {
|
||||
puts stderr "No such file: $root_filename"
|
||||
exit 1
|
||||
}
|
||||
if {![file readable $root_filename]} {
|
||||
puts stderr "File is not readable: $root_filename"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if {[catch {sqlite3 db $file_to_analyze} res]} {
|
||||
puts stderr "Cannot open datababase $root_filename: $res"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if {$bFreelistCheck || $bAll} {
|
||||
puts -nonewline "freelist-check: "
|
||||
flush stdout
|
||||
db eval BEGIN
|
||||
puts [db one {SELECT checkfreelist('main')}]
|
||||
db eval END
|
||||
}
|
||||
if {$bSummary} {
|
||||
set scale 0
|
||||
set pgsz [db one {PRAGMA page_size}]
|
||||
db eval {SELECT nPage*$pgsz AS sz, name, tbl_name
|
||||
FROM sqlite_btreeinfo
|
||||
WHERE type='index'
|
||||
ORDER BY 1 DESC, name} {
|
||||
if {$scale==0} {
|
||||
if {$sz>10000000} {
|
||||
set scale 1000000.0
|
||||
set unit MB
|
||||
} else {
|
||||
set scale 1000.0
|
||||
set unit KB
|
||||
}
|
||||
}
|
||||
puts [format {%7.1f %s index %s of table %s} \
|
||||
[expr {$sz/$scale}] $unit $name $tbl_name]
|
||||
}
|
||||
}
|
||||
if {$zIndex!=""} {
|
||||
check_index $zIndex $batchsize $bTrace
|
||||
}
|
||||
if {$zTable!=""} {
|
||||
foreach idx [db eval {SELECT name FROM sqlite_master
|
||||
WHERE type='index' AND rootpage>0
|
||||
AND tbl_name=$zTable}] {
|
||||
check_index $idx $batchsize $bTrace
|
||||
}
|
||||
}
|
||||
if {$bAll} {
|
||||
set allidx [db eval {SELECT name FROM sqlite_btreeinfo('main')
|
||||
WHERE type='index' AND rootpage>0
|
||||
ORDER BY nEntry}]
|
||||
foreach idx $allidx {
|
||||
check_index $idx $batchsize $bTrace
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
To run these tests, first build sqlite3_checker:
|
||||
|
||||
|
||||
> make sqlite3_checker
|
||||
|
||||
|
||||
Then run the "test.tcl" script using:
|
||||
|
||||
|
||||
> ./sqlite3_checker --test $path/test.tcl
|
||||
|
||||
|
||||
Optionally add the full pathnames of individual *.test modules
|
||||
@@ -0,0 +1,92 @@
|
||||
# 2017-10-11
|
||||
|
||||
set testprefix checkfreelist
|
||||
|
||||
do_execsql_test 1.0 {
|
||||
PRAGMA page_size=1024;
|
||||
CREATE TABLE t1(a, b);
|
||||
}
|
||||
|
||||
do_execsql_test 1.2 { SELECT checkfreelist('main') } {ok}
|
||||
do_execsql_test 1.3 {
|
||||
WITH s(i) AS (
|
||||
SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10000
|
||||
)
|
||||
INSERT INTO t1 SELECT randomblob(400), randomblob(400) FROM s;
|
||||
DELETE FROM t1 WHERE rowid%3;
|
||||
PRAGMA freelist_count;
|
||||
} {6726}
|
||||
|
||||
do_execsql_test 1.4 { SELECT checkfreelist('main') } {ok}
|
||||
do_execsql_test 1.5 {
|
||||
WITH freelist_trunk(i, d, n) AS (
|
||||
SELECT 1, NULL, sqlite_readint32(data, 32) FROM sqlite_dbpage WHERE pgno=1
|
||||
UNION ALL
|
||||
SELECT n, data, sqlite_readint32(data)
|
||||
FROM freelist_trunk, sqlite_dbpage WHERE pgno=n
|
||||
)
|
||||
SELECT i FROM freelist_trunk WHERE i!=1;
|
||||
} {
|
||||
10009 9715 9343 8969 8595 8222 7847 7474 7102 6727 6354 5982 5608 5234
|
||||
4860 4487 4112 3740 3367 2992 2619 2247 1872 1499 1125 752 377 5
|
||||
}
|
||||
|
||||
do_execsql_test 1.6 { SELECT checkfreelist('main') } {ok}
|
||||
|
||||
proc set_int {blob idx newval} {
|
||||
binary scan $blob I* ints
|
||||
lset ints $idx $newval
|
||||
binary format I* $ints
|
||||
}
|
||||
db func set_int set_int
|
||||
|
||||
proc get_int {blob idx} {
|
||||
binary scan $blob I* ints
|
||||
lindex $ints $idx
|
||||
}
|
||||
db func get_int get_int
|
||||
|
||||
do_execsql_test 1.7 {
|
||||
BEGIN;
|
||||
UPDATE sqlite_dbpage
|
||||
SET data = set_int(data, 1, get_int(data, 1)-1)
|
||||
WHERE pgno=4860;
|
||||
SELECT checkfreelist('main');
|
||||
ROLLBACK;
|
||||
} {{free-list count mismatch: actual=6725 header=6726}}
|
||||
|
||||
do_execsql_test 1.8 {
|
||||
BEGIN;
|
||||
UPDATE sqlite_dbpage
|
||||
SET data = set_int(data, 5, (SELECT * FROM pragma_page_count)+1)
|
||||
WHERE pgno=4860;
|
||||
SELECT checkfreelist('main');
|
||||
ROLLBACK;
|
||||
} {{leaf page 10092 is out of range (child 3 of trunk page 4860)}}
|
||||
|
||||
do_execsql_test 1.9 {
|
||||
BEGIN;
|
||||
UPDATE sqlite_dbpage
|
||||
SET data = set_int(data, 5, 0)
|
||||
WHERE pgno=4860;
|
||||
SELECT checkfreelist('main');
|
||||
ROLLBACK;
|
||||
} {{leaf page 0 is out of range (child 3 of trunk page 4860)}}
|
||||
|
||||
do_execsql_test 1.10 {
|
||||
BEGIN;
|
||||
UPDATE sqlite_dbpage
|
||||
SET data = set_int(data, get_int(data, 1)+1, 0)
|
||||
WHERE pgno=5;
|
||||
SELECT checkfreelist('main');
|
||||
ROLLBACK;
|
||||
} {{leaf page 0 is out of range (child 247 of trunk page 5)}}
|
||||
|
||||
do_execsql_test 1.11 {
|
||||
BEGIN;
|
||||
UPDATE sqlite_dbpage
|
||||
SET data = set_int(data, 1, 249)
|
||||
WHERE pgno=5;
|
||||
SELECT checkfreelist('main');
|
||||
ROLLBACK;
|
||||
} {{leaf count out of range (249) on trunk page 5}}
|
||||
@@ -0,0 +1,349 @@
|
||||
# 2017-10-11
|
||||
#
|
||||
set testprefix checkindex
|
||||
|
||||
do_execsql_test 1.0 {
|
||||
CREATE TABLE t1(a, b);
|
||||
CREATE INDEX i1 ON t1(a);
|
||||
INSERT INTO t1 VALUES('one', 2);
|
||||
INSERT INTO t1 VALUES('two', 4);
|
||||
INSERT INTO t1 VALUES('three', 6);
|
||||
INSERT INTO t1 VALUES('four', 8);
|
||||
INSERT INTO t1 VALUES('five', 10);
|
||||
|
||||
CREATE INDEX i2 ON t1(a DESC);
|
||||
} {}
|
||||
|
||||
proc incr_index_check {idx nStep} {
|
||||
set Q {
|
||||
SELECT errmsg, current_key FROM incremental_index_check($idx, $after)
|
||||
LIMIT $nStep
|
||||
}
|
||||
|
||||
set res [list]
|
||||
while {1} {
|
||||
unset -nocomplain current_key
|
||||
set res1 [db eval $Q]
|
||||
if {[llength $res1]==0} break
|
||||
set res [concat $res $res1]
|
||||
set after [lindex $res end]
|
||||
}
|
||||
|
||||
return $res
|
||||
}
|
||||
|
||||
proc do_index_check_test {tn idx res} {
|
||||
uplevel [list do_execsql_test $tn.1 "
|
||||
SELECT errmsg, current_key FROM incremental_index_check('$idx');
|
||||
" $res]
|
||||
|
||||
uplevel [list do_test $tn.2 "incr_index_check $idx 1" [list {*}$res]]
|
||||
uplevel [list do_test $tn.3 "incr_index_check $idx 2" [list {*}$res]]
|
||||
uplevel [list do_test $tn.4 "incr_index_check $idx 5" [list {*}$res]]
|
||||
}
|
||||
|
||||
|
||||
do_execsql_test 1.2.1 {
|
||||
SELECT rowid, errmsg IS NULL, current_key FROM incremental_index_check('i1');
|
||||
} {
|
||||
1 1 'five',5
|
||||
2 1 'four',4
|
||||
3 1 'one',1
|
||||
4 1 'three',3
|
||||
5 1 'two',2
|
||||
}
|
||||
do_execsql_test 1.2.2 {
|
||||
SELECT errmsg IS NULL, current_key, index_name, after_key, scanner_sql
|
||||
FROM incremental_index_check('i1') LIMIT 1;
|
||||
} {
|
||||
1
|
||||
'five',5
|
||||
i1
|
||||
{}
|
||||
{SELECT (SELECT a IS i.i0 FROM 't1' AS t WHERE "rowid" COLLATE BINARY IS i.i1), quote(i0)||','||quote(i1) FROM (SELECT (a) AS i0, ("rowid" COLLATE BINARY) AS i1 FROM 't1' INDEXED BY 'i1' ORDER BY 1,2) AS i}
|
||||
}
|
||||
|
||||
do_index_check_test 1.3 i1 {
|
||||
{} 'five',5
|
||||
{} 'four',4
|
||||
{} 'one',1
|
||||
{} 'three',3
|
||||
{} 'two',2
|
||||
}
|
||||
|
||||
do_index_check_test 1.4 i2 {
|
||||
{} 'two',2
|
||||
{} 'three',3
|
||||
{} 'one',1
|
||||
{} 'four',4
|
||||
{} 'five',5
|
||||
}
|
||||
|
||||
do_test 1.5 {
|
||||
set tblroot [db one { SELECT rootpage FROM sqlite_master WHERE name='t1' }]
|
||||
sqlite3_imposter db main $tblroot {CREATE TABLE xt1(a,b)}
|
||||
db eval {
|
||||
UPDATE xt1 SET a='six' WHERE rowid=3;
|
||||
DELETE FROM xt1 WHERE rowid = 5;
|
||||
}
|
||||
sqlite3_imposter db main
|
||||
} {}
|
||||
|
||||
do_index_check_test 1.6 i1 {
|
||||
{row missing} 'five',5
|
||||
{} 'four',4
|
||||
{} 'one',1
|
||||
{row data mismatch} 'three',3
|
||||
{} 'two',2
|
||||
}
|
||||
|
||||
do_index_check_test 1.7 i2 {
|
||||
{} 'two',2
|
||||
{row data mismatch} 'three',3
|
||||
{} 'one',1
|
||||
{} 'four',4
|
||||
{row missing} 'five',5
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
do_execsql_test 2.0 {
|
||||
|
||||
CREATE TABLE t2(a INTEGER PRIMARY KEY, b, c, d);
|
||||
|
||||
INSERT INTO t2 VALUES(1, NULL, 1, 1);
|
||||
INSERT INTO t2 VALUES(2, 1, NULL, 1);
|
||||
INSERT INTO t2 VALUES(3, 1, 1, NULL);
|
||||
|
||||
INSERT INTO t2 VALUES(4, 2, 2, 1);
|
||||
INSERT INTO t2 VALUES(5, 2, 2, 2);
|
||||
INSERT INTO t2 VALUES(6, 2, 2, 3);
|
||||
|
||||
INSERT INTO t2 VALUES(7, 2, 2, 1);
|
||||
INSERT INTO t2 VALUES(8, 2, 2, 2);
|
||||
INSERT INTO t2 VALUES(9, 2, 2, 3);
|
||||
|
||||
CREATE INDEX i3 ON t2(b, c, d);
|
||||
CREATE INDEX i4 ON t2(b DESC, c DESC, d DESC);
|
||||
CREATE INDEX i5 ON t2(d, c DESC, b);
|
||||
} {}
|
||||
|
||||
do_index_check_test 2.1 i3 {
|
||||
{} NULL,1,1,1
|
||||
{} 1,NULL,1,2
|
||||
{} 1,1,NULL,3
|
||||
{} 2,2,1,4
|
||||
{} 2,2,1,7
|
||||
{} 2,2,2,5
|
||||
{} 2,2,2,8
|
||||
{} 2,2,3,6
|
||||
{} 2,2,3,9
|
||||
}
|
||||
|
||||
do_index_check_test 2.2 i4 {
|
||||
{} 2,2,3,6
|
||||
{} 2,2,3,9
|
||||
{} 2,2,2,5
|
||||
{} 2,2,2,8
|
||||
{} 2,2,1,4
|
||||
{} 2,2,1,7
|
||||
{} 1,1,NULL,3
|
||||
{} 1,NULL,1,2
|
||||
{} NULL,1,1,1
|
||||
}
|
||||
|
||||
do_index_check_test 2.3 i5 {
|
||||
{} NULL,1,1,3
|
||||
{} 1,2,2,4
|
||||
{} 1,2,2,7
|
||||
{} 1,1,NULL,1
|
||||
{} 1,NULL,1,2
|
||||
{} 2,2,2,5
|
||||
{} 2,2,2,8
|
||||
{} 3,2,2,6
|
||||
{} 3,2,2,9
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
do_execsql_test 3.0 {
|
||||
|
||||
CREATE TABLE t3(w, x, y, z PRIMARY KEY) WITHOUT ROWID;
|
||||
CREATE INDEX t3wxy ON t3(w, x, y);
|
||||
CREATE INDEX t3wxy2 ON t3(w DESC, x DESC, y DESC);
|
||||
|
||||
INSERT INTO t3 VALUES(NULL, NULL, NULL, 1);
|
||||
INSERT INTO t3 VALUES(NULL, NULL, NULL, 2);
|
||||
INSERT INTO t3 VALUES(NULL, NULL, NULL, 3);
|
||||
|
||||
INSERT INTO t3 VALUES('a', NULL, NULL, 4);
|
||||
INSERT INTO t3 VALUES('a', NULL, NULL, 5);
|
||||
INSERT INTO t3 VALUES('a', NULL, NULL, 6);
|
||||
|
||||
INSERT INTO t3 VALUES('a', 'b', NULL, 7);
|
||||
INSERT INTO t3 VALUES('a', 'b', NULL, 8);
|
||||
INSERT INTO t3 VALUES('a', 'b', NULL, 9);
|
||||
|
||||
} {}
|
||||
|
||||
do_index_check_test 3.1 t3wxy {
|
||||
{} NULL,NULL,NULL,1 {} NULL,NULL,NULL,2 {} NULL,NULL,NULL,3
|
||||
{} 'a',NULL,NULL,4 {} 'a',NULL,NULL,5 {} 'a',NULL,NULL,6
|
||||
{} 'a','b',NULL,7 {} 'a','b',NULL,8 {} 'a','b',NULL,9
|
||||
}
|
||||
do_index_check_test 3.2 t3wxy2 {
|
||||
{} 'a','b',NULL,7 {} 'a','b',NULL,8 {} 'a','b',NULL,9
|
||||
{} 'a',NULL,NULL,4 {} 'a',NULL,NULL,5 {} 'a',NULL,NULL,6
|
||||
{} NULL,NULL,NULL,1 {} NULL,NULL,NULL,2 {} NULL,NULL,NULL,3
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Test with an index that uses non-default collation sequences.
|
||||
#
|
||||
do_execsql_test 4.0 {
|
||||
CREATE TABLE t4(a INTEGER PRIMARY KEY, c1 TEXT, c2 TEXT);
|
||||
INSERT INTO t4 VALUES(1, 'aaa', 'bbb');
|
||||
INSERT INTO t4 VALUES(2, 'AAA', 'CCC');
|
||||
INSERT INTO t4 VALUES(3, 'aab', 'ddd');
|
||||
INSERT INTO t4 VALUES(4, 'AAB', 'EEE');
|
||||
|
||||
CREATE INDEX t4cc ON t4(c1 COLLATE nocase, c2 COLLATE nocase);
|
||||
}
|
||||
|
||||
do_index_check_test 4.1 t4cc {
|
||||
{} 'aaa','bbb',1
|
||||
{} 'AAA','CCC',2
|
||||
{} 'aab','ddd',3
|
||||
{} 'AAB','EEE',4
|
||||
}
|
||||
|
||||
do_test 4.2 {
|
||||
set tblroot [db one { SELECT rootpage FROM sqlite_master WHERE name='t4' }]
|
||||
sqlite3_imposter db main $tblroot \
|
||||
{CREATE TABLE xt4(a INTEGER PRIMARY KEY, c1 TEXT, c2 TEXT)}
|
||||
|
||||
db eval {
|
||||
UPDATE xt4 SET c1='hello' WHERE rowid=2;
|
||||
DELETE FROM xt4 WHERE rowid = 3;
|
||||
}
|
||||
sqlite3_imposter db main
|
||||
} {}
|
||||
|
||||
do_index_check_test 4.3 t4cc {
|
||||
{} 'aaa','bbb',1
|
||||
{row data mismatch} 'AAA','CCC',2
|
||||
{row missing} 'aab','ddd',3
|
||||
{} 'AAB','EEE',4
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Test an index on an expression.
|
||||
#
|
||||
do_execsql_test 5.0 {
|
||||
CREATE TABLE t5(x INTEGER PRIMARY KEY, y TEXT, UNIQUE(y));
|
||||
INSERT INTO t5 VALUES(1, '{"x":1, "y":1}');
|
||||
INSERT INTO t5 VALUES(2, '{"x":2, "y":2}');
|
||||
INSERT INTO t5 VALUES(3, '{"x":3, "y":3}');
|
||||
INSERT INTO t5 VALUES(4, '{"w":4, "z":4}');
|
||||
INSERT INTO t5 VALUES(5, '{"x":5, "y":5}');
|
||||
|
||||
CREATE INDEX t5x ON t5( json_extract(y, '$.x') );
|
||||
CREATE INDEX t5y ON t5( json_extract(y, '$.y') DESC );
|
||||
}
|
||||
|
||||
do_index_check_test 5.1.1 t5x {
|
||||
{} NULL,4 {} 1,1 {} 2,2 {} 3,3 {} 5,5
|
||||
}
|
||||
|
||||
do_index_check_test 5.1.2 t5y {
|
||||
{} 5,5 {} 3,3 {} 2,2 {} 1,1 {} NULL,4
|
||||
}
|
||||
|
||||
do_index_check_test 5.1.3 sqlite_autoindex_t5_1 {
|
||||
{} {'{"w":4, "z":4}',4}
|
||||
{} {'{"x":1, "y":1}',1}
|
||||
{} {'{"x":2, "y":2}',2}
|
||||
{} {'{"x":3, "y":3}',3}
|
||||
{} {'{"x":5, "y":5}',5}
|
||||
}
|
||||
|
||||
do_test 5.2 {
|
||||
set tblroot [db one { SELECT rootpage FROM sqlite_master WHERE name='t5' }]
|
||||
sqlite3_imposter db main $tblroot \
|
||||
{CREATE TABLE xt5(a INTEGER PRIMARY KEY, c1 TEXT);}
|
||||
db eval {
|
||||
UPDATE xt5 SET c1='{"x":22, "y":11}' WHERE rowid=1;
|
||||
DELETE FROM xt5 WHERE rowid = 4;
|
||||
}
|
||||
sqlite3_imposter db main
|
||||
} {}
|
||||
|
||||
do_index_check_test 5.3.1 t5x {
|
||||
{row missing} NULL,4
|
||||
{row data mismatch} 1,1
|
||||
{} 2,2
|
||||
{} 3,3
|
||||
{} 5,5
|
||||
}
|
||||
|
||||
do_index_check_test 5.3.2 sqlite_autoindex_t5_1 {
|
||||
{row missing} {'{"w":4, "z":4}',4}
|
||||
{row data mismatch} {'{"x":1, "y":1}',1}
|
||||
{} {'{"x":2, "y":2}',2}
|
||||
{} {'{"x":3, "y":3}',3}
|
||||
{} {'{"x":5, "y":5}',5}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
do_execsql_test 6.0 {
|
||||
CREATE TABLE t6(x INTEGER PRIMARY KEY, y, z);
|
||||
CREATE INDEX t6x1 ON t6(y, /* one,two,three */ z);
|
||||
CREATE INDEX t6x2 ON t6(z, -- hello,world,
|
||||
y);
|
||||
|
||||
CREATE INDEX t6x3 ON t6(z -- hello,world
|
||||
, y);
|
||||
|
||||
INSERT INTO t6 VALUES(1, 2, 3);
|
||||
INSERT INTO t6 VALUES(4, 5, 6);
|
||||
}
|
||||
|
||||
do_index_check_test 6.1 t6x1 {
|
||||
{} 2,3,1
|
||||
{} 5,6,4
|
||||
}
|
||||
do_index_check_test 6.2 t6x2 {
|
||||
{} 3,2,1
|
||||
{} 6,5,4
|
||||
}
|
||||
do_index_check_test 6.2 t6x3 {
|
||||
{} 3,2,1
|
||||
{} 6,5,4
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
do_execsql_test 7.0 {
|
||||
CREATE TABLE t7(x INTEGER PRIMARY KEY, y, z);
|
||||
INSERT INTO t7 VALUES(1, 1, 1);
|
||||
INSERT INTO t7 VALUES(2, 2, 0);
|
||||
INSERT INTO t7 VALUES(3, 3, 1);
|
||||
INSERT INTO t7 VALUES(4, 4, 0);
|
||||
|
||||
CREATE INDEX t7i1 ON t7(y) WHERE z=1;
|
||||
CREATE INDEX t7i2 ON t7(y) /* hello,world */ WHERE z=1;
|
||||
CREATE INDEX t7i3 ON t7(y) WHERE -- yep
|
||||
z=1;
|
||||
CREATE INDEX t7i4 ON t7(y) WHERE z=1 -- yep;
|
||||
}
|
||||
do_index_check_test 7.1 t7i1 {
|
||||
{} 1,1 {} 3,3
|
||||
}
|
||||
do_index_check_test 7.2 t7i2 {
|
||||
{} 1,1 {} 3,3
|
||||
}
|
||||
do_index_check_test 7.3 t7i3 {
|
||||
{} 1,1 {} 3,3
|
||||
}
|
||||
do_index_check_test 7.4 t7i4 {
|
||||
{} 1,1 {} 3,3
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Run this script using
|
||||
#
|
||||
# sqlite3_checker --test $thisscript $testscripts
|
||||
#
|
||||
# The $testscripts argument is optional. If omitted, all *.test files
|
||||
# in the same directory as $thisscript are run.
|
||||
#
|
||||
set NTEST 0
|
||||
set NERR 0
|
||||
|
||||
|
||||
# Invoke the do_test procedure to run a single test
|
||||
#
|
||||
# The $expected parameter is the expected result. The result is the return
|
||||
# value from the last TCL command in $cmd.
|
||||
#
|
||||
# Normally, $expected must match exactly. But if $expected is of the form
|
||||
# "/regexp/" then regular expression matching is used. If $expected is
|
||||
# "~/regexp/" then the regular expression must NOT match. If $expected is
|
||||
# of the form "#/value-list/" then each term in value-list must be numeric
|
||||
# and must approximately match the corresponding numeric term in $result.
|
||||
# Values must match within 10%. Or if the $expected term is A..B then the
|
||||
# $result term must be in between A and B.
|
||||
#
|
||||
proc do_test {name cmd expected} {
|
||||
if {[info exists ::testprefix]} {
|
||||
set name "$::testprefix$name"
|
||||
}
|
||||
|
||||
incr ::NTEST
|
||||
puts -nonewline $name...
|
||||
flush stdout
|
||||
|
||||
if {[catch {uplevel #0 "$cmd;\n"} result]} {
|
||||
puts -nonewline $name...
|
||||
puts "\nError: $result"
|
||||
incr ::NERR
|
||||
} else {
|
||||
set ok [expr {[string compare $result $expected]==0}]
|
||||
if {!$ok} {
|
||||
puts "\n! $name expected: \[$expected\]\n! $name got: \[$result\]"
|
||||
incr ::NERR
|
||||
} else {
|
||||
puts " Ok"
|
||||
}
|
||||
}
|
||||
flush stdout
|
||||
}
|
||||
|
||||
#
|
||||
# do_execsql_test TESTNAME SQL RES
|
||||
#
|
||||
proc do_execsql_test {testname sql {result {}}} {
|
||||
uplevel [list do_test $testname [list db eval $sql] [list {*}$result]]
|
||||
}
|
||||
|
||||
if {[llength $argv]==0} {
|
||||
set dir [file dirname $argv0]
|
||||
set argv [glob -nocomplain $dir/*.test]
|
||||
}
|
||||
foreach testfile $argv {
|
||||
file delete -force test.db
|
||||
sqlite3 db test.db
|
||||
source $testfile
|
||||
catch {db close}
|
||||
}
|
||||
puts "$NERR errors out of $NTEST tests"
|
||||
Reference in New Issue
Block a user