/ Check-in [4f9520a9]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add new test file fts5_test_mi.c, containing an implementation of a function similar to FTS4 matchinfo() for FTS5.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 4f9520a9dc9c667b7fda5b0822de2bf48184ac99
User & Date: dan 2015-08-04 20:29:00
Context
2015-08-05
07:43
Remove all references to "docid" within fts5 source code and comments. Replace with "rowid". check-in: dffd358f user: dan tags: trunk
2015-08-04
20:29
Add new test file fts5_test_mi.c, containing an implementation of a function similar to FTS4 matchinfo() for FTS5. check-in: 4f9520a9 user: dan tags: trunk
19:06
Improve the usage comment on sqlite3_analyzer: show the available switches. check-in: 783f78e3 user: drh tags: trunk
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

Changes to Makefile.in.

   411    411   TESTSRC += \
   412    412     $(TOP)/ext/misc/amatch.c \
   413    413     $(TOP)/ext/misc/closure.c \
   414    414     $(TOP)/ext/misc/eval.c \
   415    415     $(TOP)/ext/misc/fileio.c \
   416    416     $(TOP)/ext/misc/fuzzer.c \
   417    417     $(TOP)/ext/fts5/fts5_tcl.c \
          418  +  $(TOP)/ext/fts5/fts5_test_mi.c \
   418    419     $(TOP)/ext/misc/ieee754.c \
   419    420     $(TOP)/ext/misc/nextchar.c \
   420    421     $(TOP)/ext/misc/percentile.c \
   421    422     $(TOP)/ext/misc/regexp.c \
   422    423     $(TOP)/ext/misc/spellfix.c \
   423    424     $(TOP)/ext/misc/totype.c \
   424    425     $(TOP)/ext/misc/wholenumber.c

Changes to Makefile.msc.

  1077   1077     $(TOP)\ext\misc\amatch.c \
  1078   1078     $(TOP)\ext\misc\closure.c \
  1079   1079     $(TOP)\ext\misc\eval.c \
  1080   1080     $(TOP)\ext\misc\fileio.c \
  1081   1081     $(TOP)\ext\misc\fuzzer.c \
  1082   1082     fts5.c \
  1083   1083     $(TOP)\ext\fts5\fts5_tcl.c \
         1084  +  $(TOP)\ext\fts5\fts5_test_mi.c \
  1084   1085     $(TOP)\ext\misc\ieee754.c \
  1085   1086     $(TOP)\ext\misc\nextchar.c \
  1086   1087     $(TOP)\ext\misc\percentile.c \
  1087   1088     $(TOP)\ext\misc\regexp.c \
  1088   1089     $(TOP)\ext\misc\spellfix.c \
  1089   1090     $(TOP)\ext\misc\totype.c \
  1090   1091     $(TOP)\ext\misc\wholenumber.c

Changes to ext/fts5/fts5.h.

    56     56   **   the FTS5 table.
    57     57   **
    58     58   **   If parameter iCol is greater than or equal to the number of columns
    59     59   **   in the table, SQLITE_RANGE is returned. Or, if an error occurs (e.g.
    60     60   **   an OOM condition or IO error), an appropriate SQLite error code is 
    61     61   **   returned.
    62     62   **
    63         -** xColumnCount(pFts, iCol, pnToken):
           63  +** xColumnCount(pFts):
           64  +**   Return the number of columns in the table.
           65  +**
           66  +** xColumnSize(pFts, iCol, pnToken):
    64     67   **   If parameter iCol is less than zero, set output variable *pnToken
    65     68   **   to the total number of tokens in the current row. Or, if iCol is
    66     69   **   non-negative but less than the number of columns in the table, set
    67     70   **   *pnToken to the number of tokens in column iCol of the current row.
    68     71   **
    69     72   **   If parameter iCol is greater than or equal to the number of columns
    70     73   **   in the table, SQLITE_RANGE is returned. Or, if an error occurs (e.g.
    71     74   **   an OOM condition or IO error), an appropriate SQLite error code is 
    72     75   **   returned.
    73     76   **
    74         -** xColumnSize:
    75         -**   Reports the size in tokens of a column value from the current row.
    76         -**
    77     77   ** xColumnText:
    78     78   **   This function attempts to retrieve the text of column iCol of the
    79     79   **   current document. If successful, (*pz) is set to point to a buffer
    80     80   **   containing the text in utf-8 encoding, (*pn) is set to the size in bytes
    81     81   **   (not characters) of the buffer and SQLITE_OK is returned. Otherwise,
    82     82   **   if an error occurs, an SQLite error code is returned and the final values
    83     83   **   of (*pz) and (*pn) are undefined.

Changes to ext/fts5/fts5_tcl.c.

   941    941     }
   942    942     z = Tcl_GetStringFromObj(objv[2], &n);
   943    943   
   944    944     iVal = f5t_fts5HashKey(nSlot, z, n);
   945    945     Tcl_SetObjResult(interp, Tcl_NewIntObj(iVal));
   946    946     return TCL_OK;
   947    947   }
          948  +
          949  +static int f5tRegisterMatchinfo(
          950  +  void * clientData,
          951  +  Tcl_Interp *interp,
          952  +  int objc,
          953  +  Tcl_Obj *CONST objv[]
          954  +){
          955  +  int rc;
          956  +  sqlite3 *db = 0;
          957  +
          958  +  if( objc!=2 ){
          959  +    Tcl_WrongNumArgs(interp, 1, objv, "DB");
          960  +    return TCL_ERROR;
          961  +  }
          962  +  if( f5tDbPointer(interp, objv[1], &db) ){
          963  +    return TCL_ERROR;
          964  +  }
          965  +
          966  +  rc = sqlite3Fts5TestRegisterMatchinfo(db);
          967  +  if( rc!=SQLITE_OK ){
          968  +    Tcl_SetResult(interp, (char*)sqlite3ErrName(rc), TCL_VOLATILE);
          969  +    return TCL_ERROR;
          970  +  }
          971  +  return TCL_OK;
          972  +}
   948    973   
   949    974   /*
   950    975   ** Entry point.
   951    976   */
   952    977   int Fts5tcl_Init(Tcl_Interp *interp){
   953    978     static struct Cmd {
   954    979       char *zName;
   955    980       Tcl_ObjCmdProc *xProc;
   956    981       int bTokenizeCtx;
   957    982     } aCmd[] = {
   958         -    { "sqlite3_fts5_create_tokenizer", f5tCreateTokenizer, 1 },
   959         -    { "sqlite3_fts5_token",            f5tTokenizerReturn, 1 },
   960         -    { "sqlite3_fts5_tokenize",         f5tTokenize, 0 },
   961         -    { "sqlite3_fts5_create_function",  f5tCreateFunction, 0 },
   962         -    { "sqlite3_fts5_may_be_corrupt",   f5tMayBeCorrupt, 0 },
   963         -    { "sqlite3_fts5_token_hash",       f5tTokenHash, 0 }
          983  +    { "sqlite3_fts5_create_tokenizer",   f5tCreateTokenizer, 1 },
          984  +    { "sqlite3_fts5_token",              f5tTokenizerReturn, 1 },
          985  +    { "sqlite3_fts5_tokenize",           f5tTokenize, 0 },
          986  +    { "sqlite3_fts5_create_function",    f5tCreateFunction, 0 },
          987  +    { "sqlite3_fts5_may_be_corrupt",     f5tMayBeCorrupt, 0 },
          988  +    { "sqlite3_fts5_token_hash",         f5tTokenHash, 0 },
          989  +    { "sqlite3_fts5_register_matchinfo", f5tRegisterMatchinfo, 0 }
   964    990     };
   965    991     int i;
   966    992     F5tTokenizerContext *pContext;
   967    993   
   968    994     pContext = (F5tTokenizerContext*)ckalloc(sizeof(F5tTokenizerContext));
   969    995     memset(pContext, 0, sizeof(*pContext));
   970    996   

Added ext/fts5/fts5_test_mi.c.

            1  +/*
            2  +** 2015 Aug 04
            3  +**
            4  +** The author disclaims copyright to this source code.  In place of
            5  +** a legal notice, here is a blessing:
            6  +**
            7  +**    May you do good and not evil.
            8  +**    May you find forgiveness for yourself and forgive others.
            9  +**    May you share freely, never taking more than you give.
           10  +**
           11  +******************************************************************************
           12  +**
           13  +** This file contains test code only, it is not included in release 
           14  +** versions of FTS5. It contains the implementation of an FTS5 auxiliary
           15  +** function very similar to the FTS4 function matchinfo():
           16  +**
           17  +**     https://www.sqlite.org/fts3.html#matchinfo
           18  +**
           19  +** Known differences are that:
           20  +**
           21  +**  1) this function uses the FTS5 definition of "matchable phrase", which
           22  +**     excludes any phrases that are part of an expression sub-tree that
           23  +**     does not match the current row. This comes up for MATCH queries 
           24  +**     such as:
           25  +**
           26  +**         "a OR (b AND c)"
           27  +**
           28  +**     In FTS4, if a single row contains instances of tokens "a" and "c", 
           29  +**     but not "b", all instances of "c" are considered matches. In FTS5,
           30  +**     they are not (as the "b AND c" sub-tree does not match the current
           31  +**     row.
           32  +**
           33  +**  2) ...
           34  +**     
           35  +** This file exports a single function that may be called to register the
           36  +** matchinfo() implementation with a database handle:
           37  +**
           38  +**   int sqlite3Fts5TestRegisterMatchinfo(sqlite3 *db);
           39  +*/
           40  +
           41  +
           42  +#ifdef SQLITE_TEST
           43  +#ifdef SQLITE_ENABLE_FTS5
           44  +
           45  +#include "fts5.h"
           46  +#include <tcl.h>
           47  +#include <assert.h>
           48  +#include <string.h>
           49  +
           50  +typedef struct Fts5MatchinfoCtx Fts5MatchinfoCtx;
           51  +typedef unsigned int u32;
           52  +
           53  +struct Fts5MatchinfoCtx {
           54  +  int nCol;                       /* Number of cols in FTS5 table */
           55  +  int nPhrase;                    /* Number of phrases in FTS5 query */
           56  +  char *zArg;                     /* nul-term'd copy of 2nd arg */
           57  +  int nRet;                       /* Number of elements in aRet[] */
           58  +  u32 *aRet;                      /* Array of 32-bit unsigned ints to return */
           59  +};
           60  +
           61  +
           62  +
           63  +/*
           64  +** Return a pointer to the fts5_api pointer for database connection db.
           65  +** If an error occurs, return NULL and leave an error in the database 
           66  +** handle (accessible using sqlite3_errcode()/errmsg()).
           67  +*/
           68  +static fts5_api *fts5_api_from_db(sqlite3 *db){
           69  +  fts5_api *pRet = 0;
           70  +  sqlite3_stmt *pStmt = 0;
           71  +
           72  +  if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5()", -1, &pStmt, 0)
           73  +   && SQLITE_ROW==sqlite3_step(pStmt) 
           74  +   && sizeof(pRet)==sqlite3_column_bytes(pStmt, 0)
           75  +  ){
           76  +    memcpy(&pRet, sqlite3_column_blob(pStmt, 0), sizeof(pRet));
           77  +  }
           78  +  sqlite3_finalize(pStmt);
           79  +  return pRet;
           80  +}
           81  +
           82  +
           83  +/*
           84  +** Argument f should be a flag accepted by matchinfo() (a valid character
           85  +** in the string passed as the second argument). If it is not, 0 is 
           86  +** returned. Otherwise, if f is a valid matchinfo flag, the value returned
           87  +** is the number of 32-bit integers added to the output array if the
           88  +** table has nCol columns and the query nPhrase phrases.
           89  +*/
           90  +static int fts5MatchinfoFlagsize(int nCol, int nPhrase, char f){
           91  +  int ret = 0;
           92  +  switch( f ){
           93  +    case 'p': ret = 1; break;
           94  +    case 'c': ret = 1; break;
           95  +    case 'x': ret = 3 * nCol * nPhrase; break;
           96  +    case 'y': ret = nCol * nPhrase; break;
           97  +    case 'b': ret = ((nCol + 31) / 32) * nPhrase; break;
           98  +    case 'n': ret = 1; break;
           99  +    case 'a': ret = nCol; break;
          100  +    case 'l': ret = nCol; break;
          101  +    case 's': ret = nCol; break;
          102  +  }
          103  +  return ret;
          104  +}
          105  +
          106  +static int fts5MatchinfoIter(
          107  +  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
          108  +  Fts5Context *pFts,              /* First arg to pass to pApi functions */
          109  +  Fts5MatchinfoCtx *p,
          110  +  int(*x)(const Fts5ExtensionApi*,Fts5Context*,Fts5MatchinfoCtx*,char,u32*)
          111  +){
          112  +  int i;
          113  +  int n = 0;
          114  +  int rc = SQLITE_OK;
          115  +  char f;
          116  +  for(i=0; (f = p->zArg[i]); i++){
          117  +    rc = x(pApi, pFts, p, f, &p->aRet[n]);
          118  +    if( rc!=SQLITE_OK ) break;
          119  +    n += fts5MatchinfoFlagsize(p->nCol, p->nPhrase, f);
          120  +  }
          121  +  return rc;
          122  +}
          123  +
          124  +static int fts5MatchinfoXCb(
          125  +  const Fts5ExtensionApi *pApi,
          126  +  Fts5Context *pFts,
          127  +  void *pUserData
          128  +){
          129  +  u32 *aOut = (u32*)pUserData;
          130  +  int nCol = pApi->xColumnCount(pFts);
          131  +  int nInst;
          132  +  int iPrev = -1;
          133  +  int rc;
          134  +  int i;
          135  +
          136  +  rc = pApi->xInstCount(pFts, &nInst);
          137  +  for(i=0; rc==SQLITE_OK && i<nInst; i++){
          138  +    int iPhrase, iCol, iOff;
          139  +    rc = pApi->xInst(pFts, i, &iPhrase, &iCol, &iOff);
          140  +    aOut[iCol*3 + 1]++;
          141  +    if( iCol!=iPrev ) aOut[iCol*3 + 2]++;
          142  +    iPrev = iCol;
          143  +  }
          144  +
          145  +  return rc;
          146  +}
          147  +
          148  +static int fts5MatchinfoGlobalCb(
          149  +  const Fts5ExtensionApi *pApi,
          150  +  Fts5Context *pFts,
          151  +  Fts5MatchinfoCtx *p,
          152  +  char f,
          153  +  u32 *aOut
          154  +){
          155  +  int rc = SQLITE_OK;
          156  +  switch( f ){
          157  +    case 'p':
          158  +      aOut[0] = p->nPhrase; 
          159  +      break;
          160  +
          161  +    case 'c':
          162  +      aOut[0] = p->nCol; 
          163  +      break;
          164  +
          165  +    case 'x': {
          166  +      int i;
          167  +      for(i=0; i<p->nPhrase && rc==SQLITE_OK; i++){
          168  +        void *pPtr = (void*)&aOut[i * p->nCol * 3];
          169  +        rc = pApi->xQueryPhrase(pFts, i, pPtr, fts5MatchinfoXCb);
          170  +      }
          171  +      break;
          172  +    }
          173  +
          174  +    case 'n': {
          175  +      sqlite3_int64 nRow;
          176  +      rc = pApi->xRowCount(pFts, &nRow);
          177  +      aOut[0] = (u32)nRow;
          178  +      break;
          179  +    }
          180  +
          181  +    case 'a': {
          182  +      sqlite3_int64 nRow = 0;
          183  +      rc = pApi->xRowCount(pFts, &nRow);
          184  +      if( nRow==0 ){
          185  +        memset(aOut, 0, sizeof(u32) * p->nCol);
          186  +      }else{
          187  +        int i;
          188  +        for(i=0; rc==SQLITE_OK && i<p->nCol; i++){
          189  +          sqlite3_int64 nToken;
          190  +          rc = pApi->xColumnTotalSize(pFts, i, &nToken);
          191  +          if( rc==SQLITE_OK){
          192  +            aOut[i] = (u32)((2*nToken + nRow) / (2*nRow));
          193  +          }
          194  +        }
          195  +      }
          196  +      break;
          197  +    }
          198  +
          199  +  }
          200  +  return rc;
          201  +}
          202  +
          203  +static int fts5MatchinfoLocalCb(
          204  +  const Fts5ExtensionApi *pApi,
          205  +  Fts5Context *pFts,
          206  +  Fts5MatchinfoCtx *p,
          207  +  char f,
          208  +  u32 *aOut
          209  +){
          210  +  int i;
          211  +  int rc = SQLITE_OK;
          212  +
          213  +  switch( f ){
          214  +    case 'b': 
          215  +    case 'x':
          216  +    case 'y': {
          217  +      int nInst;
          218  +      int nMul = (f=='x' ? 3 : 1);
          219  +
          220  +      if( f=='b' ){
          221  +        int nInt = ((p->nCol + 31) / 32) * p->nPhrase;
          222  +        for(i=0; i<nInt; i++) aOut[i] = 0;
          223  +      }else{
          224  +        for(i=0; i<(p->nCol*p->nPhrase); i++) aOut[i*nMul] = 0;
          225  +      }
          226  +
          227  +      rc = pApi->xInstCount(pFts, &nInst);
          228  +      for(i=0; rc==SQLITE_OK && i<nInst; i++){
          229  +        int iPhrase, iOff, iCol = 0;
          230  +        rc = pApi->xInst(pFts, i, &iPhrase, &iCol, &iOff);
          231  +        if( f=='b' ){
          232  +          aOut[iPhrase * ((p->nCol+31)/32) + iCol/32] |= ((u32)1 << (iCol%32));
          233  +        }else{
          234  +          aOut[nMul * (iCol + iPhrase * p->nCol)]++;
          235  +        }
          236  +      }
          237  +
          238  +      break;
          239  +    }
          240  +
          241  +    case 'l': {
          242  +      for(i=0; rc==SQLITE_OK && i<p->nCol; i++){
          243  +        int nToken;
          244  +        rc = pApi->xColumnSize(pFts, i, &nToken);
          245  +        aOut[i] = (u32)nToken;
          246  +      }
          247  +      break;
          248  +    }
          249  +
          250  +    case 's':
          251  +      memset(aOut, 0, sizeof(u32) * p->nCol);
          252  +      break;
          253  +  }
          254  +  return rc;
          255  +}
          256  + 
          257  +static Fts5MatchinfoCtx *fts5MatchinfoNew(
          258  +  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
          259  +  Fts5Context *pFts,              /* First arg to pass to pApi functions */
          260  +  sqlite3_context *pCtx,          /* Context for returning error message */
          261  +  const char *zArg                /* Matchinfo flag string */
          262  +){
          263  +  Fts5MatchinfoCtx *p;
          264  +  int nCol;
          265  +  int nPhrase;
          266  +  int i;
          267  +  int nInt;
          268  +  int nByte;
          269  +  int rc;
          270  +
          271  +  nCol = pApi->xColumnCount(pFts);
          272  +  nPhrase = pApi->xPhraseCount(pFts);
          273  +
          274  +  nInt = 0;
          275  +  for(i=0; zArg[i]; i++){
          276  +    int n = fts5MatchinfoFlagsize(nCol, nPhrase, zArg[i]);
          277  +    if( n==0 ){
          278  +      char *zErr = sqlite3_mprintf("unrecognized matchinfo flag: %c", zArg[i]);
          279  +      sqlite3_result_error(pCtx, zErr, -1);
          280  +      sqlite3_free(zErr);
          281  +      return 0;
          282  +    }
          283  +    nInt += n;
          284  +  }
          285  +
          286  +  nByte = sizeof(Fts5MatchinfoCtx)          /* The struct itself */
          287  +         + sizeof(u32) * nInt               /* The p->aRet[] array */
          288  +         + (i+1);                           /* The p->zArg string */
          289  +  p = (Fts5MatchinfoCtx*)sqlite3_malloc(nByte);
          290  +  if( p==0 ){
          291  +    sqlite3_result_error_nomem(pCtx);
          292  +    return 0;
          293  +  }
          294  +  memset(p, 0, nByte);
          295  +
          296  +  p->nCol = nCol;
          297  +  p->nPhrase = nPhrase;
          298  +  p->aRet = (u32*)&p[1];
          299  +  p->nRet = nInt;
          300  +  p->zArg = (char*)&p->aRet[nInt];
          301  +  memcpy(p->zArg, zArg, i);
          302  +
          303  +  rc = fts5MatchinfoIter(pApi, pFts, p, fts5MatchinfoGlobalCb);
          304  +  if( rc!=SQLITE_OK ){
          305  +    sqlite3_result_error_code(pCtx, rc);
          306  +    sqlite3_free(p);
          307  +    p = 0;
          308  +  }
          309  +
          310  +  return p;
          311  +}
          312  +
          313  +static void fts5MatchinfoFunc(
          314  +  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
          315  +  Fts5Context *pFts,              /* First arg to pass to pApi functions */
          316  +  sqlite3_context *pCtx,          /* Context for returning result/error */
          317  +  int nVal,                       /* Number of values in apVal[] array */
          318  +  sqlite3_value **apVal           /* Array of trailing arguments */
          319  +){
          320  +  const char *zArg;
          321  +  Fts5MatchinfoCtx *p;
          322  +  int rc;
          323  +
          324  +  if( nVal>0 ){
          325  +    zArg = (const char*)sqlite3_value_text(apVal[0]);
          326  +  }else{
          327  +    zArg = "pcx";
          328  +  }
          329  +
          330  +  p = (Fts5MatchinfoCtx*)pApi->xGetAuxdata(pFts, 0);
          331  +  if( p==0 || sqlite3_stricmp(zArg, p->zArg) ){
          332  +    p = fts5MatchinfoNew(pApi, pFts, pCtx, zArg);
          333  +    pApi->xSetAuxdata(pFts, p, sqlite3_free);
          334  +    if( p==0 ) return;
          335  +  }
          336  +
          337  +  rc = fts5MatchinfoIter(pApi, pFts, p, fts5MatchinfoLocalCb);
          338  +  if( rc!=SQLITE_OK ){
          339  +    sqlite3_result_error_code(pCtx, rc);
          340  +  }else{
          341  +    /* No errors has occured, so return a copy of the array of integers. */
          342  +    int nByte = p->nRet * sizeof(u32);
          343  +    sqlite3_result_blob(pCtx, (void*)p->aRet, nByte, SQLITE_TRANSIENT);
          344  +  }
          345  +}
          346  +
          347  +int sqlite3Fts5TestRegisterMatchinfo(sqlite3 *db){
          348  +  int rc;                         /* Return code */
          349  +  fts5_api *pApi;                 /* FTS5 API functions */
          350  +
          351  +  /* Extract the FTS5 API pointer from the database handle. The 
          352  +  ** fts5_api_from_db() function above is copied verbatim from the 
          353  +  ** FTS5 documentation. Refer there for details. */
          354  +  pApi = fts5_api_from_db(db);
          355  +
          356  +  /* If fts5_api_from_db() returns NULL, then either FTS5 is not registered
          357  +  ** with this database handle, or an error (OOM perhaps?) has occurred.
          358  +  **
          359  +  ** Also check that the fts5_api object is version 1 or newer (there 
          360  +  ** is no actual version of FTS5 that would return an API object of version
          361  +  ** 0, but FTS5 extensions should check the API version before using it). */
          362  +  if( pApi==0 || pApi->iVersion<1 ){
          363  +    return SQLITE_ERROR;
          364  +  }
          365  +
          366  +  /* Register the implementation of matchinfo() */
          367  +  rc = pApi->xCreateFunction(pApi, "matchinfo", 0, fts5MatchinfoFunc, 0);
          368  +
          369  +  return rc;
          370  +}
          371  +
          372  +#endif /* SQLITE_ENABLE_FTS5 */
          373  +#endif /* SQLITE_TEST */
          374  +

Changes to main.mk.

   301    301     $(TOP)/ext/misc/percentile.c \
   302    302     $(TOP)/ext/misc/regexp.c \
   303    303     $(TOP)/ext/misc/spellfix.c \
   304    304     $(TOP)/ext/misc/totype.c \
   305    305     $(TOP)/ext/misc/wholenumber.c \
   306    306     $(TOP)/ext/misc/vfslog.c \
   307    307     $(TOP)/ext/fts5/fts5_tcl.c \
          308  +  $(TOP)/ext/fts5/fts5_test_mi.c \
   308    309     fts5.c
   309    310   
   310    311   
   311    312   #TESTSRC += $(TOP)/ext/fts2/fts2_tokenizer.c
   312    313   #TESTSRC += $(TOP)/ext/fts3/fts3_tokenizer.c
   313    314   
   314    315   TESTSRC2 = \