/* ** 2014 Jun 09 ** ** 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 is an SQLite module implementing full-text search. */ #ifdef SQLITE_ENABLE_FTS5 #include "fts5Int.h" #define FTS5_DEFAULT_PAGE_SIZE 1000 #define FTS5_DEFAULT_AUTOMERGE 4 #define FTS5_DEFAULT_CRISISMERGE 16 /* Maximum allowed page size */ #define FTS5_MAX_PAGE_SIZE (128*1024) static int fts5_iswhitespace(char x){ return (x==' '); } static int fts5_isopenquote(char x){ return (x=='"' || x=='\'' || x=='[' || x=='`'); } /* ** Argument pIn points to a character that is part of a nul-terminated ** string. Return a pointer to the first character following *pIn in ** the string that is not a white-space character. */ static const char *fts5ConfigSkipWhitespace(const char *pIn){ const char *p = pIn; if( p ){ while( fts5_iswhitespace(*p) ){ p++; } } return p; } /* ** Argument pIn points to a character that is part of a nul-terminated ** string. Return a pointer to the first character following *pIn in ** the string that is not a "bareword" character. */ static const char *fts5ConfigSkipBareword(const char *pIn){ const char *p = pIn; while( *p && *p!=' ' && *p!=':' && *p!='!' && *p!='@' && *p!='#' && *p!='$' && *p!='%' && *p!='^' && *p!='&' && *p!='*' && *p!='(' && *p!=')' && *p!='=' ){ p++; } if( p==pIn ) p = 0; return p; } static int fts5_isdigit(char a){ return (a>='0' && a<='9'); } static const char *fts5ConfigSkipLiteral(const char *pIn){ const char *p = pIn; if( p ){ switch( *p ){ case 'n': case 'N': if( sqlite3_strnicmp("null", p, 4)==0 ){ p = &p[4]; }else{ p = 0; } break; case 'x': case 'X': p++; if( *p=='\'' ){ p++; while( (*p>='a' && *p<='f') || (*p>='A' && *p<='F') || (*p>='0' && *p<='9') ){ p++; } if( *p=='\'' && 0==((p-pIn)%2) ){ p++; }else{ p = 0; } }else{ p = 0; } break; case '\'': p++; while( p ){ if( *p=='\'' ){ p++; if( *p!='\'' ) break; } p++; if( *p==0 ) p = 0; } break; default: /* maybe a number */ if( *p=='+' || *p=='-' ) p++; while( fts5_isdigit(*p) ) p++; /* At this point, if the literal was an integer, the parse is ** finished. Or, if it is a floating point value, it may continue ** with either a decimal point or an 'E' character. */ if( *p=='.' && fts5_isdigit(p[1]) ){ p += 2; while( fts5_isdigit(*p) ) p++; } if( p==pIn ) p = 0; break; } } return p; } /* ** The first character of the string pointed to by argument z is guaranteed ** to be an open-quote character (see function fts5_isopenquote()). ** ** This function searches for the corresponding close-quote character within ** the string and, if found, dequotes the string in place and adds a new ** nul-terminator byte. ** ** If the close-quote is found, the value returned is the byte offset of ** the character immediately following it. Or, if the close-quote is not ** found, -1 is returned. If -1 is returned, the buffer is left in an ** undefined state. */ static int fts5Dequote(char *z){ char q; int iIn = 1; int iOut = 0; q = z[0]; /* Set stack variable q to the close-quote character */ assert( q=='[' || q=='\'' || q=='"' || q=='`' ); if( q=='[' ) q = ']'; while( z[iIn] ){ if( z[iIn]==q ){ if( z[iIn+1]!=q ){ /* Character iIn was the close quote. */ z[iOut] = '\0'; return iIn+1; }else{ /* Character iIn and iIn+1 form an escaped quote character. Skip ** the input cursor past both and copy a single quote character ** to the output buffer. */ iIn += 2; z[iOut++] = q; } }else{ z[iOut++] = z[iIn++]; } } /* Did not find the close-quote character. Return -1. */ z[iOut] = '\0'; return -1; } /* ** Convert an SQL-style quoted string into a normal string by removing ** the quote characters. The conversion is done in-place. If the ** input does not begin with a quote character, then this routine ** is a no-op. ** ** Examples: ** ** "abc" becomes abc ** 'xyz' becomes xyz ** [pqr] becomes pqr ** `mno` becomes mno */ void sqlite3Fts5Dequote(char *z){ char quote; /* Quote character (if any ) */ assert( 0==fts5_iswhitespace(z[0]) ); quote = z[0]; if( quote=='[' || quote=='\'' || quote=='"' || quote=='`' ){ fts5Dequote(z); } } /* ** Duplicate the string passed as the only argument into a buffer allocated ** by sqlite3_malloc(). ** ** Return 0 if an OOM error is encountered. */ static char *fts5Strdup(int *pRc, const char *z){ char *pRet = 0; if( *pRc==SQLITE_OK ){ pRet = sqlite3_mprintf("%s", z); if( pRet==0 ) *pRc = SQLITE_NOMEM; } return pRet; } /* ** Argument z points to a nul-terminated string containing an SQL identifier. ** This function returns a copy of the identifier enclosed in backtick ** quotes. */ static char *fts5EscapeName(int *pRc, const char *z){ char *pRet = 0; if( *pRc==SQLITE_OK ){ int n = strlen(z); pRet = (char*)sqlite3_malloc(2 + 2*n + 1); if( pRet==0 ){ *pRc = SQLITE_NOMEM; }else{ int i; char *p = pRet; *p++ = '`'; for(i=0; iaPrefix ){ *pzErr = sqlite3_mprintf("multiple prefix=... directives"); rc = SQLITE_ERROR; }else{ pConfig->aPrefix = sqlite3Fts5MallocZero(&rc, nByte); } p = zArg; while( rc==SQLITE_OK && p[0] ){ int nPre = 0; while( p[0]==' ' ) p++; while( p[0]>='0' && p[0]<='9' && nPre<1000 ){ nPre = nPre*10 + (p[0] - '0'); p++; } while( p[0]==' ' ) p++; if( p[0]==',' ){ p++; }else if( p[0] ){ *pzErr = sqlite3_mprintf("malformed prefix=... directive"); rc = SQLITE_ERROR; } if( rc==SQLITE_OK && (nPre==0 || nPre>=1000) ){ *pzErr = sqlite3_mprintf("prefix length out of range: %d", nPre); rc = SQLITE_ERROR; } pConfig->aPrefix[pConfig->nPrefix] = nPre; pConfig->nPrefix++; } return rc; } if( sqlite3_strnicmp("tokenize", zCmd, nCmd)==0 ){ int rc = SQLITE_OK; const char *p = (const char*)zArg; int nArg = strlen(zArg) + 1; char **azArg = sqlite3Fts5MallocZero(&rc, sizeof(char*) * nArg); char *pDel = sqlite3Fts5MallocZero(&rc, nArg * 2); char *pSpace = pDel; if( azArg && pSpace ){ if( pConfig->pTok ){ *pzErr = sqlite3_mprintf("multiple tokenize=... directives"); rc = SQLITE_ERROR; }else{ for(nArg=0; p && *p; nArg++){ const char *p2 = fts5ConfigSkipWhitespace(p); if( p2 && *p2=='\'' ){ p = fts5ConfigSkipLiteral(p2); }else{ p = fts5ConfigSkipBareword(p2); } if( p ){ memcpy(pSpace, p2, p-p2); azArg[nArg] = pSpace; sqlite3Fts5Dequote(pSpace); pSpace += (p - p2) + 1; p = fts5ConfigSkipWhitespace(p); } } if( p==0 ){ *pzErr = sqlite3_mprintf("parse error in tokenize directive"); rc = SQLITE_ERROR; }else{ rc = sqlite3Fts5GetTokenizer(pGlobal, (const char**)azArg, nArg, &pConfig->pTok, &pConfig->pTokApi ); if( rc!=SQLITE_OK ){ *pzErr = sqlite3_mprintf("error in tokenizer constructor"); } } } } sqlite3_free(azArg); sqlite3_free(pDel); return rc; } if( sqlite3_strnicmp("content", zCmd, nCmd)==0 ){ int rc = SQLITE_OK; if( pConfig->eContent!=FTS5_CONTENT_NORMAL ){ *pzErr = sqlite3_mprintf("multiple content=... directives"); rc = SQLITE_ERROR; }else{ if( zArg[0] ){ pConfig->eContent = FTS5_CONTENT_EXTERNAL; pConfig->zContent = sqlite3_mprintf("%Q.%Q", pConfig->zDb, zArg); }else{ pConfig->eContent = FTS5_CONTENT_NONE; pConfig->zContent = sqlite3_mprintf( "%Q.'%q_docsize'", pConfig->zDb, pConfig->zName ); } if( pConfig->zContent==0 ) rc = SQLITE_NOMEM; } return rc; } if( sqlite3_strnicmp("content_rowid", zCmd, nCmd)==0 ){ int rc = SQLITE_OK; if( pConfig->zContentRowid ){ *pzErr = sqlite3_mprintf("multiple content_rowid=... directives"); rc = SQLITE_ERROR; }else{ pConfig->zContentRowid = fts5Strdup(&rc, zArg); } return rc; } *pzErr = sqlite3_mprintf("unrecognized option: \"%.*s\"", nCmd, zCmd); return SQLITE_ERROR; } /* ** Allocate an instance of the default tokenizer ("simple") at ** Fts5Config.pTokenizer. Return SQLITE_OK if successful, or an SQLite error ** code if an error occurs. */ static int fts5ConfigDefaultTokenizer(Fts5Global *pGlobal, Fts5Config *pConfig){ assert( pConfig->pTok==0 && pConfig->pTokApi==0 ); return sqlite3Fts5GetTokenizer( pGlobal, 0, 0, &pConfig->pTok, &pConfig->pTokApi ); } /* ** Gobble up the first bareword or quoted word from the input buffer zIn. ** Return a pointer to the character immediately following the last in ** the gobbled word if successful, or a NULL pointer otherwise (failed ** to find close-quote character). ** ** Before returning, set pzOut to point to a new buffer containing a ** nul-terminated, dequoted copy of the gobbled word. If the word was ** quoted, *pbQuoted is also set to 1 before returning. ** ** If *pRc is other than SQLITE_OK when this function is called, it is ** a no-op (NULL is returned). Otherwise, if an OOM occurs within this ** function, *pRc is set to SQLITE_NOMEM before returning. *pRc is *not* ** set if a parse error (failed to find close quote) occurs. */ static const char *fts5ConfigGobbleWord( int *pRc, /* IN/OUT: Error code */ const char *zIn, /* Buffer to gobble string/bareword from */ char **pzOut, /* OUT: malloc'd buffer containing str/bw */ int *pbQuoted /* OUT: Set to true if dequoting required */ ){ const char *zRet = 0; *pbQuoted = 0; *pzOut = 0; if( *pRc==SQLITE_OK ){ int nIn = strlen(zIn); char *zOut = sqlite3_malloc(nIn+1); if( zOut==0 ){ *pRc = SQLITE_NOMEM; }else{ memcpy(zOut, zIn, nIn+1); if( fts5_isopenquote(zOut[0]) ){ int ii = fts5Dequote(zOut); if( ii>0 ) zRet = &zIn[ii]; *pbQuoted = 1; }else{ zRet = fts5ConfigSkipBareword(zIn); zOut[zRet-zIn] = '\0'; } } if( zRet==0 ){ sqlite3_free(zOut); }else{ *pzOut = zOut; } } return zRet; } static int fts5ConfigParseColumn( Fts5Config *p, char *zCol, char *zArg, char **pzErr ){ int rc = SQLITE_OK; if( 0==sqlite3_stricmp(zCol, FTS5_RANK_NAME) || 0==sqlite3_stricmp(zCol, FTS5_ROWID_NAME) ){ *pzErr = sqlite3_mprintf("reserved fts5 column name: %s", zCol); rc = SQLITE_ERROR; }else if( zArg ){ if( 0==sqlite3_stricmp(zArg, "unindexed") ){ p->abUnindexed[p->nCol] = 1; }else{ *pzErr = sqlite3_mprintf("unrecognized column option: %s", zArg); rc = SQLITE_ERROR; } } p->azCol[p->nCol++] = zCol; return rc; } /* ** Populate the Fts5Config.zContentExprlist string. */ static int fts5ConfigMakeExprlist(Fts5Config *p){ int i; int rc = SQLITE_OK; Fts5Buffer buf = {0, 0, 0}; const char *zSep = ""; sqlite3Fts5BufferAppendPrintf(&rc, &buf, "T.%Q", p->zContentRowid); if( p->eContent!=FTS5_CONTENT_NONE ){ for(i=0; inCol; i++){ if( p->eContent==FTS5_CONTENT_EXTERNAL ){ sqlite3Fts5BufferAppendPrintf(&rc, &buf, ", T.%Q", p->azCol[i]); }else{ sqlite3Fts5BufferAppendPrintf(&rc, &buf, ", T.c%d", i); } } } assert( p->zContentExprlist==0 ); p->zContentExprlist = (char*)buf.p; return rc; } /* ** Arguments nArg/azArg contain the string arguments passed to the xCreate ** or xConnect method of the virtual table. This function attempts to ** allocate an instance of Fts5Config containing the results of parsing ** those arguments. ** ** If successful, SQLITE_OK is returned and *ppOut is set to point to the ** new Fts5Config object. If an error occurs, an SQLite error code is ** returned, *ppOut is set to NULL and an error message may be left in ** *pzErr. It is the responsibility of the caller to eventually free any ** such error message using sqlite3_free(). */ int sqlite3Fts5ConfigParse( Fts5Global *pGlobal, sqlite3 *db, int nArg, /* Number of arguments */ const char **azArg, /* Array of nArg CREATE VIRTUAL TABLE args */ Fts5Config **ppOut, /* OUT: Results of parse */ char **pzErr /* OUT: Error message */ ){ int rc = SQLITE_OK; /* Return code */ Fts5Config *pRet; /* New object to return */ int i; int nByte; *ppOut = pRet = (Fts5Config*)sqlite3_malloc(sizeof(Fts5Config)); if( pRet==0 ) return SQLITE_NOMEM; memset(pRet, 0, sizeof(Fts5Config)); pRet->db = db; pRet->iCookie = -1; nByte = nArg * (sizeof(char*) + sizeof(u8)); pRet->azCol = (char**)sqlite3Fts5MallocZero(&rc, nByte); pRet->abUnindexed = (u8*)&pRet->azCol[nArg]; pRet->zDb = fts5Strdup(&rc, azArg[1]); pRet->zName = fts5Strdup(&rc, azArg[2]); if( rc==SQLITE_OK && sqlite3_stricmp(pRet->zName, FTS5_RANK_NAME)==0 ){ *pzErr = sqlite3_mprintf("reserved fts5 table name: %s", pRet->zName); rc = SQLITE_ERROR; } for(i=3; rc==SQLITE_OK && ipTok==0 ){ rc = fts5ConfigDefaultTokenizer(pGlobal, pRet); } /* If no zContent option was specified, fill in the default values. */ if( rc==SQLITE_OK && pRet->eContent==FTS5_CONTENT_NORMAL ){ pRet->zContent = sqlite3_mprintf("%Q.'%q_content'", pRet->zDb, pRet->zName); if( pRet->zContent==0 ){ rc = SQLITE_NOMEM; }else{ sqlite3_free(pRet->zContentRowid); pRet->zContentRowid = 0; } } if( rc==SQLITE_OK && pRet->zContentRowid==0 ){ pRet->zContentRowid = fts5Strdup(&rc, "rowid"); } /* Formulate the zContentExprlist text */ if( rc==SQLITE_OK ){ rc = fts5ConfigMakeExprlist(pRet); } if( rc!=SQLITE_OK ){ sqlite3Fts5ConfigFree(pRet); *ppOut = 0; } return rc; } /* ** Free the configuration object passed as the only argument. */ void sqlite3Fts5ConfigFree(Fts5Config *pConfig){ if( pConfig ){ int i; if( pConfig->pTok && pConfig->pTokApi->xDelete ){ pConfig->pTokApi->xDelete(pConfig->pTok); } sqlite3_free(pConfig->zDb); sqlite3_free(pConfig->zName); for(i=0; inCol; i++){ sqlite3_free(pConfig->azCol[i]); } sqlite3_free(pConfig->azCol); sqlite3_free(pConfig->aPrefix); sqlite3_free(pConfig->zRank); sqlite3_free(pConfig->zRankArgs); sqlite3_free(pConfig->zContent); sqlite3_free(pConfig->zContentRowid); sqlite3_free(pConfig->zContentExprlist); sqlite3_free(pConfig); } } /* ** Call sqlite3_declare_vtab() based on the contents of the configuration ** object passed as the only argument. Return SQLITE_OK if successful, or ** an SQLite error code if an error occurs. */ int sqlite3Fts5ConfigDeclareVtab(Fts5Config *pConfig){ int i; int rc; char *zSql; char *zOld; zSql = (char*)sqlite3_mprintf("CREATE TABLE x("); for(i=0; zSql && inCol; i++){ zOld = zSql; zSql = sqlite3_mprintf("%s%s%Q", zOld, (i==0?"":", "), pConfig->azCol[i]); sqlite3_free(zOld); } if( zSql ){ zOld = zSql; zSql = sqlite3_mprintf("%s, %Q HIDDEN, %s HIDDEN)", zOld, pConfig->zName, FTS5_RANK_NAME ); sqlite3_free(zOld); } if( zSql==0 ){ rc = SQLITE_NOMEM; }else{ rc = sqlite3_declare_vtab(pConfig->db, zSql); sqlite3_free(zSql); } return rc; } /* ** Tokenize the text passed via the second and third arguments. ** ** The callback is invoked once for each token in the input text. The ** arguments passed to it are, in order: ** ** void *pCtx // Copy of 4th argument to sqlite3Fts5Tokenize() ** const char *pToken // Pointer to buffer containing token ** int nToken // Size of token in bytes ** int iStart // Byte offset of start of token within input text ** int iEnd // Byte offset of end of token within input text ** int iPos // Position of token in input (first token is 0) ** ** If the callback returns a non-zero value the tokenization is abandoned ** and no further callbacks are issued. ** ** This function returns SQLITE_OK if successful or an SQLite error code ** if an error occurs. If the tokenization was abandoned early because ** the callback returned SQLITE_DONE, this is not an error and this function ** still returns SQLITE_OK. Or, if the tokenization was abandoned early ** because the callback returned another non-zero value, it is assumed ** to be an SQLite error code and returned to the caller. */ int sqlite3Fts5Tokenize( Fts5Config *pConfig, /* FTS5 Configuration object */ const char *pText, int nText, /* Text to tokenize */ void *pCtx, /* Context passed to xToken() */ int (*xToken)(void*, const char*, int, int, int) /* Callback */ ){ return pConfig->pTokApi->xTokenize(pConfig->pTok, pCtx, pText, nText, xToken); } /* ** Argument pIn points to the first character in what is expected to be ** a comma-separated list of SQL literals followed by a ')' character. ** If it actually is this, return a pointer to the ')'. Otherwise, return ** NULL to indicate a parse error. */ static const char *fts5ConfigSkipArgs(const char *pIn){ const char *p = pIn; while( 1 ){ p = fts5ConfigSkipWhitespace(p); p = fts5ConfigSkipLiteral(p); p = fts5ConfigSkipWhitespace(p); if( p==0 || *p==')' ) break; if( *p!=',' ){ p = 0; break; } p++; } return p; } /* ** Parameter zIn contains a rank() function specification. The format of ** this is: ** ** + Bareword (function name) ** + Open parenthesis - "(" ** + Zero or more SQL literals in a comma separated list ** + Close parenthesis - ")" */ int sqlite3Fts5ConfigParseRank( const char *zIn, /* Input string */ char **pzRank, /* OUT: Rank function name */ char **pzRankArgs /* OUT: Rank function arguments */ ){ const char *p = zIn; const char *pRank; char *zRank = 0; char *zRankArgs = 0; int rc = SQLITE_OK; *pzRank = 0; *pzRankArgs = 0; p = fts5ConfigSkipWhitespace(p); pRank = p; p = fts5ConfigSkipBareword(p); if( p ){ zRank = sqlite3Fts5MallocZero(&rc, 1 + p - pRank); if( zRank ) memcpy(zRank, pRank, p-pRank); }else{ rc = SQLITE_ERROR; } if( rc==SQLITE_OK ){ p = fts5ConfigSkipWhitespace(p); if( *p!='(' ) rc = SQLITE_ERROR; p++; } if( rc==SQLITE_OK ){ const char *pArgs; p = fts5ConfigSkipWhitespace(p); pArgs = p; if( *p!=')' ){ p = fts5ConfigSkipArgs(p); if( p==0 ){ rc = SQLITE_ERROR; }else if( p!=pArgs ){ zRankArgs = sqlite3Fts5MallocZero(&rc, 1 + p - pArgs); if( zRankArgs ) memcpy(zRankArgs, pArgs, p-pArgs); } } } if( rc!=SQLITE_OK ){ sqlite3_free(zRank); assert( zRankArgs==0 ); }else{ *pzRank = zRank; *pzRankArgs = zRankArgs; } return rc; } int sqlite3Fts5ConfigSetValue( Fts5Config *pConfig, const char *zKey, sqlite3_value *pVal, int *pbBadkey ){ int rc = SQLITE_OK; if( 0==sqlite3_stricmp(zKey, "cookie") ){ pConfig->iCookie = sqlite3_value_int(pVal); } else if( 0==sqlite3_stricmp(zKey, "pgsz") ){ int pgsz = 0; if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ pgsz = sqlite3_value_int(pVal); } if( pgsz<=0 || pgsz>FTS5_MAX_PAGE_SIZE ){ if( pbBadkey ) *pbBadkey = 1; }else{ pConfig->pgsz = pgsz; } } else if( 0==sqlite3_stricmp(zKey, "automerge") ){ int nAutomerge = -1; if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ nAutomerge = sqlite3_value_int(pVal); } if( nAutomerge<0 || nAutomerge>64 ){ if( pbBadkey ) *pbBadkey = 1; }else{ if( nAutomerge==1 ) nAutomerge = FTS5_DEFAULT_AUTOMERGE; pConfig->nAutomerge = nAutomerge; } } else if( 0==sqlite3_stricmp(zKey, "crisismerge") ){ int nCrisisMerge = -1; if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ nCrisisMerge = sqlite3_value_int(pVal); } if( nCrisisMerge<0 ){ if( pbBadkey ) *pbBadkey = 1; }else{ if( nCrisisMerge<=1 ) nCrisisMerge = FTS5_DEFAULT_CRISISMERGE; pConfig->nCrisisMerge = nCrisisMerge; } } else if( 0==sqlite3_stricmp(zKey, "rank") ){ const char *zIn = (const char*)sqlite3_value_text(pVal); char *zRank; char *zRankArgs; rc = sqlite3Fts5ConfigParseRank(zIn, &zRank, &zRankArgs); if( rc==SQLITE_OK ){ sqlite3_free(pConfig->zRank); sqlite3_free(pConfig->zRankArgs); pConfig->zRank = zRank; pConfig->zRankArgs = zRankArgs; }else if( rc==SQLITE_ERROR ){ rc = SQLITE_OK; if( pbBadkey ) *pbBadkey = 1; } }else{ if( pbBadkey ) *pbBadkey = 1; } return rc; } /* ** Load the contents of the %_config table into memory. */ int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){ const char *zSelect = "SELECT k, v FROM %Q.'%q_config'"; char *zSql; sqlite3_stmt *p = 0; int rc; /* Set default values */ pConfig->pgsz = FTS5_DEFAULT_PAGE_SIZE; pConfig->nAutomerge = FTS5_DEFAULT_AUTOMERGE; pConfig->nCrisisMerge = FTS5_DEFAULT_CRISISMERGE; zSql = sqlite3_mprintf(zSelect, pConfig->zDb, pConfig->zName); if( zSql==0 ){ rc = SQLITE_NOMEM; }else{ rc = sqlite3_prepare_v2(pConfig->db, zSql, -1, &p, 0); sqlite3_free(zSql); } assert( rc==SQLITE_OK || p==0 ); if( rc==SQLITE_OK ){ while( SQLITE_ROW==sqlite3_step(p) ){ const char *zK = (const char*)sqlite3_column_text(p, 0); sqlite3_value *pVal = sqlite3_column_value(p, 1); sqlite3Fts5ConfigSetValue(pConfig, zK, pVal, 0); } rc = sqlite3_finalize(p); } if( rc==SQLITE_OK ){ pConfig->iCookie = iCookie; } return rc; } #endif /* SQLITE_ENABLE_FTS5 */