/ Check-in [0fc0ea20]
Login

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

Overview
Comment:Add syntax to fts5 used to specify that a phrase or NEAR group should match a subset of columns. For example "[col1 col2 ...] : <phrase>".
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | fts5
Files: files | file ages | folders
SHA1: 0fc0ea20920615f3e48ea2dbe2b7dcd979b0993e
User & Date: dan 2015-05-29 15:55:30
Context
2015-05-29
19:00
Add extra tests and fixes for multi-column matches. check-in: ae6794ff user: dan tags: fts5
15:55
Add syntax to fts5 used to specify that a phrase or NEAR group should match a subset of columns. For example "[col1 col2 ...] : <phrase>". check-in: 0fc0ea20 user: dan tags: fts5
2015-05-28
19:57
Optimizations for fts5 queries that match against a specific column. check-in: b29ac50a user: dan tags: fts5
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

Changes to ext/fts5/fts5Int.h.

   507    507   */
   508    508   typedef struct Fts5Expr Fts5Expr;
   509    509   typedef struct Fts5ExprNode Fts5ExprNode;
   510    510   typedef struct Fts5Parse Fts5Parse;
   511    511   typedef struct Fts5Token Fts5Token;
   512    512   typedef struct Fts5ExprPhrase Fts5ExprPhrase;
   513    513   typedef struct Fts5ExprNearset Fts5ExprNearset;
          514  +typedef struct Fts5ExprColset Fts5ExprColset;
   514    515   
   515    516   struct Fts5Token {
   516    517     const char *p;                  /* Token text (not NULL terminated) */
   517    518     int n;                          /* Size of buffer p in bytes */
   518    519   };
   519    520   
   520    521   /* Parse a MATCH expression. */
................................................................................
   573    574   );
   574    575   
   575    576   Fts5ExprNearset *sqlite3Fts5ParseNearset(
   576    577     Fts5Parse*, 
   577    578     Fts5ExprNearset*,
   578    579     Fts5ExprPhrase* 
   579    580   );
          581  +
          582  +Fts5ExprColset *sqlite3Fts5ParseColset(
          583  +  Fts5Parse*, 
          584  +  Fts5ExprColset*, 
          585  +  Fts5Token *
          586  +);
   580    587   
   581    588   void sqlite3Fts5ParsePhraseFree(Fts5ExprPhrase*);
   582    589   void sqlite3Fts5ParseNearsetFree(Fts5ExprNearset*);
   583    590   void sqlite3Fts5ParseNodeFree(Fts5ExprNode*);
   584    591   
   585    592   void sqlite3Fts5ParseSetDistance(Fts5Parse*, Fts5ExprNearset*, Fts5Token*);
   586         -void sqlite3Fts5ParseSetColumn(Fts5Parse*, Fts5ExprNearset*, Fts5Token*);
          593  +void sqlite3Fts5ParseSetColset(Fts5Parse*, Fts5ExprNearset*, Fts5ExprColset*);
   587    594   void sqlite3Fts5ParseFinished(Fts5Parse *pParse, Fts5ExprNode *p);
   588    595   void sqlite3Fts5ParseNear(Fts5Parse *pParse, Fts5Token*);
   589    596   
   590    597   /*
   591    598   ** End of interface to code in fts5_expr.c.
   592    599   **************************************************************************/
   593    600   

Changes to ext/fts5/fts5_expr.c.

    75     75   struct Fts5ExprPhrase {
    76     76     Fts5ExprNode *pNode;            /* FTS5_STRING node this phrase is part of */
    77     77     Fts5Buffer poslist;             /* Current position list */
    78     78     int nTerm;                      /* Number of entries in aTerm[] */
    79     79     Fts5ExprTerm aTerm[0];          /* Terms that make up this phrase */
    80     80   };
    81     81   
           82  +/*
           83  +** If a NEAR() clump may only match a specific set of columns, then
           84  +** Fts5ExprNearset.pColset points to an object of the following type.
           85  +** Each entry in the aiCol[] array
           86  +*/
           87  +struct Fts5ExprColset {
           88  +  int nCol;
           89  +  int aiCol[1];
           90  +};
           91  +
    82     92   /*
    83     93   ** One or more phrases that must appear within a certain token distance of
    84     94   ** each other within each matching document.
    85     95   */
    86     96   struct Fts5ExprNearset {
    87     97     int nNear;                      /* NEAR parameter */
    88         -  int iCol;                       /* Column to search (-1 -> all columns) */
           98  +  Fts5ExprColset *pColset;        /* Columns to search (NULL -> all columns) */
    89     99     int nPhrase;                    /* Number of entries in aPhrase[] array */
    90    100     Fts5ExprPhrase *apPhrase[0];    /* Array of phrase pointers */
    91    101   };
    92    102   
    93    103   
    94    104   /*
    95    105   ** Parse context.
................................................................................
   132    142     while( fts5ExprIsspace(*z) ) z++;
   133    143   
   134    144     pToken->p = z;
   135    145     pToken->n = 1;
   136    146     switch( *z ){
   137    147       case '(':  tok = FTS5_LP;    break;
   138    148       case ')':  tok = FTS5_RP;    break;
          149  +    case '[':  tok = FTS5_LSP;   break;
          150  +    case ']':  tok = FTS5_RSP;   break;
   139    151       case ':':  tok = FTS5_COLON; break;
   140    152       case ',':  tok = FTS5_COMMA; break;
   141    153       case '+':  tok = FTS5_PLUS;  break;
   142    154       case '*':  tok = FTS5_STAR;  break;
   143    155       case '\0': tok = FTS5_EOF;   break;
   144    156   
   145    157       case '"': {
................................................................................
   271    283         pNew->nPhrase = 1;
   272    284         pNew->apExprPhrase = apPhrase;
   273    285         pNew->apExprPhrase[0] = pCopy;
   274    286   
   275    287         pNode->eType = FTS5_STRING;
   276    288         pNode->pNear = pNear;
   277    289   
   278         -      pNear->iCol = -1;
   279    290         pNear->nPhrase = 1;
   280    291         pNear->apPhrase[0] = pCopy;
   281    292   
   282    293         pCopy->nTerm = pOrig->nTerm;
   283    294         pCopy->pNode = pNode;
   284    295       }else{
   285    296         /* At least one allocation failed. Free them all. */
................................................................................
   331    342   **
   332    343   ** SQLITE_OK is returned if an error occurs, or an SQLite error code 
   333    344   ** otherwise. It is not considered an error code if the current rowid is 
   334    345   ** not a match.
   335    346   */
   336    347   static int fts5ExprPhraseIsMatch(
   337    348     Fts5Expr *pExpr,                /* Expression pPhrase belongs to */
   338         -  int iCol,                       /* If >=0, search for matches in iCol only */
          349  +  Fts5ExprColset *pColset,        /* Restrict matches to these columns */
   339    350     Fts5ExprPhrase *pPhrase,        /* Phrase object to initialize */
   340    351     int *pbMatch                    /* OUT: Set to true if really a match */
   341    352   ){
   342    353     Fts5PoslistWriter writer = {0};
   343    354     Fts5PoslistReader aStatic[4];
   344    355     Fts5PoslistReader *aIter = aStatic;
   345    356     int i;
   346    357     int rc = SQLITE_OK;
          358  +  int iCol = pColset ? pColset->aiCol[0] : -1;
   347    359   
   348    360     fts5BufferZero(&pPhrase->poslist);
   349    361   
   350    362     /* If the aStatic[] array is not large enough, allocate a large array
   351    363     ** using sqlite3_malloc(). This approach could be improved upon. */
   352    364     if( pPhrase->nTerm>(sizeof(aStatic) / sizeof(aStatic[0])) ){
   353    365       int nByte = sizeof(Fts5PoslistReader) * pPhrase->nTerm;
................................................................................
   660    672   ** contain any entries for column iCol, return 0.
   661    673   */
   662    674   static int fts5ExprExtractCol(
   663    675     const u8 **pa,                  /* IN/OUT: Pointer to poslist */
   664    676     int n,                          /* IN: Size of poslist in bytes */
   665    677     int iCol                        /* Column to extract from poslist */
   666    678   ){
   667         -  int ii;
   668    679     int iCurrent = 0;
   669    680     const u8 *p = *pa;
   670    681     const u8 *pEnd = &p[n];         /* One byte past end of position list */
   671    682     u8 prev = 0;
   672    683   
   673    684     while( iCol!=iCurrent ){
   674    685       /* Advance pointer p until it points to pEnd or an 0x01 byte that is
................................................................................
   712    723     Fts5Expr *pExpr,                /* Expression that pNear is a part of */
   713    724     Fts5ExprNode *pNode             /* The "NEAR" node (FTS5_STRING) */
   714    725   ){
   715    726     Fts5ExprNearset *pNear = pNode->pNear;
   716    727     int rc = SQLITE_OK;
   717    728   
   718    729     while( 1 ){
   719         -    int i;
   720    730   
   721    731       if( pNear->nPhrase==1 && pNear->apPhrase[0]->nTerm==1 ){
   722    732         /* If this "NEAR" object is actually a single phrase that consists 
   723    733         ** of a single term only, then grab pointers into the poslist
   724    734         ** managed by the fts5_index.c iterator object. This is much faster 
   725    735         ** than synthesizing a new poslist the way we have to for more
   726    736         ** complicated phrase or NEAR expressions.  */
   727    737         Fts5ExprPhrase *pPhrase = pNear->apPhrase[0];
   728    738         Fts5IndexIter *pIter = pPhrase->aTerm[0].pIter;
   729         -      assert( pPhrase->poslist.nSpace==0 );
   730         -      rc = sqlite3Fts5IterPoslist(pIter, 
   731         -          (const u8**)&pPhrase->poslist.p, &pPhrase->poslist.n, &pNode->iRowid
   732         -      );
          739  +      Fts5ExprColset *pColset = pNear->pColset;
          740  +      const u8 *pPos;
          741  +      int nPos;
          742  +
          743  +      rc = sqlite3Fts5IterPoslist(pIter, &pPos, &nPos, &pNode->iRowid);
   733    744   
   734    745         /* If the term may match any column, then this must be a match. 
   735    746         ** Return immediately in this case. Otherwise, try to find the
   736    747         ** part of the poslist that corresponds to the required column.
   737    748         ** If it can be found, return. If it cannot, the next iteration
   738    749         ** of the loop will test the next rowid in the database for this
   739    750         ** term.  */
   740         -      if( pNear->iCol<0 ) return rc;
          751  +      if( pColset==0 ){
          752  +        assert( pPhrase->poslist.nSpace==0 );
          753  +        pPhrase->poslist.p = (u8*)pPos;
          754  +        pPhrase->poslist.n = nPos;
          755  +      }else if( pColset->nCol==1 ){
          756  +        assert( pPhrase->poslist.nSpace==0 );
          757  +        pPhrase->poslist.n = fts5ExprExtractCol(&pPos, nPos, pColset->aiCol[0]);
          758  +        pPhrase->poslist.p = (u8*)pPos;
          759  +      }else{
          760  +        int i;
          761  +        fts5BufferZero(&pPhrase->poslist);
          762  +        for(i=0; i<pColset->nCol; i++){
          763  +          const u8 *pSub = pPos;
          764  +          int nSub = fts5ExprExtractCol(&pSub, nPos, pColset->aiCol[i]);
          765  +          if( nSub ){
          766  +            fts5BufferAppendBlob(&rc, &pPhrase->poslist, nSub, pSub);
          767  +          }
          768  +        }
          769  +      }
   741    770   
   742         -      pPhrase->poslist.n = fts5ExprExtractCol(
   743         -          (const u8**)&pPhrase->poslist.p,
   744         -          pPhrase->poslist.n,
   745         -          pNear->iCol
   746         -      );
   747    771         if( pPhrase->poslist.n ) return rc;
   748    772       }else{
          773  +      int i;
          774  +
          775  +      assert( pNear->pColset==0 || pNear->pColset->nCol==1 );
   749    776   
   750    777         /* Advance the iterators until they all point to the same rowid */
   751    778         rc = fts5ExprNearNextRowidMatch(pExpr, pNode);
   752    779         if( rc!=SQLITE_OK || pNode->bEof ) break;
   753    780   
   754    781         /* Check that each phrase in the nearset matches the current row.
   755    782         ** Populate the pPhrase->poslist buffers at the same time. If any
   756    783         ** phrase is not a match, break out of the loop early.  */
   757    784         for(i=0; rc==SQLITE_OK && i<pNear->nPhrase; i++){
   758    785           Fts5ExprPhrase *pPhrase = pNear->apPhrase[i];
   759         -        if( pPhrase->nTerm>1 || pNear->iCol>=0 ){
          786  +        if( pPhrase->nTerm>1 || pNear->pColset ){
   760    787             int bMatch = 0;
   761         -          rc = fts5ExprPhraseIsMatch(pExpr, pNear->iCol, pPhrase, &bMatch);
          788  +          rc = fts5ExprPhraseIsMatch(pExpr, pNear->pColset, pPhrase, &bMatch);
   762    789             if( bMatch==0 ) break;
   763    790           }else{
   764    791             rc = sqlite3Fts5IterPoslistBuffer(
   765    792                 pPhrase->aTerm[0].pIter, &pPhrase->poslist
   766         -              );
          793  +          );
   767    794           }
   768    795         }
   769    796   
   770    797         if( rc==SQLITE_OK && i==pNear->nPhrase ){
   771    798           int bMatch = 1;
   772    799           if( pNear->nPhrase>1 ){
   773    800             rc = fts5ExprNearIsMatch(pNear, &bMatch);
................................................................................
  1148   1175       if( pNear==0 ){
  1149   1176         int nByte = sizeof(Fts5ExprNearset) + SZALLOC * sizeof(Fts5ExprPhrase*);
  1150   1177         pRet = sqlite3_malloc(nByte);
  1151   1178         if( pRet==0 ){
  1152   1179           pParse->rc = SQLITE_NOMEM;
  1153   1180         }else{
  1154   1181           memset(pRet, 0, nByte);
  1155         -        pRet->iCol = -1;
  1156   1182         }
  1157   1183       }else if( (pNear->nPhrase % SZALLOC)==0 ){
  1158   1184         int nNew = pNear->nPhrase + SZALLOC;
  1159   1185         int nByte = sizeof(Fts5ExprNearset) + nNew * sizeof(Fts5ExprPhrase*);
  1160   1186   
  1161   1187         pRet = (Fts5ExprNearset*)sqlite3_realloc(pNear, nByte);
  1162   1188         if( pRet==0 ){
................................................................................
  1231   1257   */
  1232   1258   void sqlite3Fts5ParseNearsetFree(Fts5ExprNearset *pNear){
  1233   1259     if( pNear ){
  1234   1260       int i;
  1235   1261       for(i=0; i<pNear->nPhrase; i++){
  1236   1262         fts5ExprPhraseFree(pNear->apPhrase[i]);
  1237   1263       }
         1264  +    sqlite3_free(pNear->pColset);
  1238   1265       sqlite3_free(pNear);
  1239   1266     }
  1240   1267   }
  1241   1268   
  1242   1269   void sqlite3Fts5ParseFinished(Fts5Parse *pParse, Fts5ExprNode *p){
  1243   1270     assert( pParse->pExpr==0 );
  1244   1271     pParse->pExpr = p;
................................................................................
  1309   1336           pParse, "fts5: syntax error near \"%.*s\"", pTok->n, pTok->p
  1310   1337       );
  1311   1338     }
  1312   1339   }
  1313   1340   
  1314   1341   void sqlite3Fts5ParseSetDistance(
  1315   1342     Fts5Parse *pParse, 
  1316         -  Fts5ExprNearset *pNear, 
         1343  +  Fts5ExprNearset *pNear,
  1317   1344     Fts5Token *p
  1318   1345   ){
  1319   1346     int nNear = 0;
  1320   1347     int i;
  1321   1348     if( p->n ){
  1322   1349       for(i=0; i<p->n; i++){
  1323   1350         char c = (char)p->p[i];
................................................................................
  1331   1358       }
  1332   1359     }else{
  1333   1360       nNear = FTS5_DEFAULT_NEARDIST;
  1334   1361     }
  1335   1362     pNear->nNear = nNear;
  1336   1363   }
  1337   1364   
  1338         -void sqlite3Fts5ParseSetColumn(
  1339         -  Fts5Parse *pParse, 
  1340         -  Fts5ExprNearset *pNear, 
         1365  +/*
         1366  +** The second argument passed to this function may be NULL, or it may be
         1367  +** an existing Fts5ExprColset object. This function returns a pointer to
         1368  +** a new colset object containing the contents of (p) with new value column
         1369  +** number iCol appended. 
         1370  +**
         1371  +** If an OOM error occurs, store an error code in pParse and return NULL.
         1372  +** The old colset object (if any) is not freed in this case.
         1373  +*/
         1374  +static Fts5ExprColset *fts5ParseColset(
         1375  +  Fts5Parse *pParse,              /* Store SQLITE_NOMEM here if required */
         1376  +  Fts5ExprColset *p,              /* Existing colset object */
         1377  +  int iCol                        /* New column to add to colset object */
         1378  +){
         1379  +  int nCol = p ? p->nCol : 0;     /* Num. columns already in colset object */
         1380  +  Fts5ExprColset *pNew;           /* New colset object to return */
         1381  +
         1382  +  assert( pParse->rc==SQLITE_OK );
         1383  +  assert( iCol>=0 && iCol<pParse->pConfig->nCol );
         1384  +
         1385  +  pNew = sqlite3_realloc(p, sizeof(Fts5ExprColset) + sizeof(int)*nCol);
         1386  +  if( pNew==0 ){
         1387  +    pParse->rc = SQLITE_NOMEM;
         1388  +  }else{
         1389  +    int *aiCol = pNew->aiCol;
         1390  +    int i, j;
         1391  +    for(i=0; i<nCol; i++){
         1392  +      if( aiCol[i]==iCol ) return pNew;
         1393  +      if( aiCol[i]>iCol ) break;
         1394  +    }
         1395  +    for(j=nCol; j>i; j--){
         1396  +      aiCol[j] = aiCol[j-1];
         1397  +    }
         1398  +    aiCol[i] = iCol;
         1399  +    pNew->nCol = nCol+1;
         1400  +
         1401  +#ifndef NDEBUG
         1402  +    /* Check that the array is in order and contains no duplicate entries. */
         1403  +    for(i=1; i<pNew->nCol; i++) assert( pNew->aiCol[i]>pNew->aiCol[i-1] );
         1404  +#endif
         1405  +  }
         1406  +
         1407  +  return pNew;
         1408  +}
         1409  +
         1410  +Fts5ExprColset *sqlite3Fts5ParseColset(
         1411  +  Fts5Parse *pParse,              /* Store SQLITE_NOMEM here if required */
         1412  +  Fts5ExprColset *pColset,        /* Existing colset object */
  1341   1413     Fts5Token *p
  1342   1414   ){
         1415  +  Fts5ExprColset *pRet = 0;
         1416  +
  1343   1417     if( pParse->rc==SQLITE_OK ){
         1418  +    int iCol;
  1344   1419       char *z = 0;
  1345   1420       int rc = fts5ParseStringFromToken(p, &z);
  1346   1421       if( rc==SQLITE_OK ){
  1347   1422         Fts5Config *pConfig = pParse->pConfig;
  1348         -      int i;
  1349         -      for(i=0; i<pConfig->nCol; i++){
  1350         -        if( 0==sqlite3_stricmp(pConfig->azCol[i], z) ){
  1351         -          pNear->iCol = i;
         1423  +      sqlite3Fts5Dequote(z);
         1424  +      for(iCol=0; iCol<pConfig->nCol; iCol++){
         1425  +        if( 0==sqlite3_stricmp(pConfig->azCol[iCol], z) ){
  1352   1426             break;
  1353   1427           }
  1354   1428         }
  1355         -      if( i==pConfig->nCol ){
         1429  +      if( iCol==pConfig->nCol ){
  1356   1430           sqlite3Fts5ParseError(pParse, "no such column: %s", z);
  1357   1431         }
  1358   1432         sqlite3_free(z);
  1359   1433       }else{
  1360   1434         pParse->rc = rc;
  1361   1435       }
         1436  +
         1437  +    if( pParse->rc==SQLITE_OK ){
         1438  +      pRet = fts5ParseColset(pParse, pColset, iCol);
         1439  +    }
         1440  +  }
         1441  +
         1442  +  if( pParse->rc!=SQLITE_OK ){
         1443  +    assert( pRet==0 );
         1444  +    sqlite3_free(pColset);
         1445  +  }
         1446  +
         1447  +  return pRet;
         1448  +}
         1449  +
         1450  +void sqlite3Fts5ParseSetColset(
         1451  +  Fts5Parse *pParse, 
         1452  +  Fts5ExprNearset *pNear, 
         1453  +  Fts5ExprColset *pColset 
         1454  +){
         1455  +  if( pNear ){
         1456  +    pNear->pColset = pColset;
         1457  +  }else{
         1458  +    sqlite3_free(pColset);
  1362   1459     }
  1363   1460   }
  1364   1461   
  1365   1462   /*
  1366   1463   ** Allocate and return a new expression object. If anything goes wrong (i.e.
  1367   1464   ** OOM error), leave an error code in pParse and return NULL.
  1368   1465   */
................................................................................
  1459   1556     if( pExpr->eType==FTS5_STRING ){
  1460   1557       Fts5ExprNearset *pNear = pExpr->pNear;
  1461   1558       int i; 
  1462   1559       int iTerm;
  1463   1560   
  1464   1561       zRet = fts5PrintfAppend(zRet, "[%s ", zNearsetCmd);
  1465   1562       if( zRet==0 ) return 0;
  1466         -    if( pNear->iCol>=0 ){
  1467         -      zRet = fts5PrintfAppend(zRet, "-col %d ", pNear->iCol);
         1563  +    if( pNear->pColset ){
         1564  +      int *aiCol = pNear->pColset->aiCol;
         1565  +      int nCol = pNear->pColset->nCol;
         1566  +      if( nCol==1 ){
         1567  +        zRet = fts5PrintfAppend(zRet, "-col %d ", aiCol[0]);
         1568  +      }else{
         1569  +        zRet = fts5PrintfAppend(zRet, "-col {%d", aiCol[0]);
         1570  +        for(i=1; i<pNear->pColset->nCol; i++){
         1571  +          zRet = fts5PrintfAppend(zRet, " %d", aiCol[i]);
         1572  +        }
         1573  +        zRet = fts5PrintfAppend(zRet, "} ");
         1574  +      }
  1468   1575         if( zRet==0 ) return 0;
  1469   1576       }
  1470   1577   
  1471   1578       if( pNear->nPhrase>1 ){
  1472   1579         zRet = fts5PrintfAppend(zRet, "-near %d ", pNear->nNear);
  1473   1580         if( zRet==0 ) return 0;
  1474   1581       }
................................................................................
  1526   1633   static char *fts5ExprPrint(Fts5Config *pConfig, Fts5ExprNode *pExpr){
  1527   1634     char *zRet = 0;
  1528   1635     if( pExpr->eType==FTS5_STRING ){
  1529   1636       Fts5ExprNearset *pNear = pExpr->pNear;
  1530   1637       int i; 
  1531   1638       int iTerm;
  1532   1639   
  1533         -    if( pNear->iCol>=0 ){
  1534         -      zRet = fts5PrintfAppend(zRet, "%s : ", pConfig->azCol[pNear->iCol]);
         1640  +    if( pNear->pColset ){
         1641  +      int iCol = pNear->pColset->aiCol[0];
         1642  +      zRet = fts5PrintfAppend(zRet, "%s : ", pConfig->azCol[iCol]);
  1535   1643         if( zRet==0 ) return 0;
  1536   1644       }
  1537   1645   
  1538   1646       if( pNear->nPhrase>1 ){
  1539   1647         zRet = fts5PrintfAppend(zRet, "NEAR(");
  1540   1648         if( zRet==0 ) return 0;
  1541   1649       }

Changes to ext/fts5/fts5_index.c.

  4475   4475   }
  4476   4476   
  4477   4477   /*
  4478   4478   ** Move to the next matching rowid. 
  4479   4479   */
  4480   4480   int sqlite3Fts5IterNext(Fts5IndexIter *pIter){
  4481   4481     assert( pIter->pIndex->rc==SQLITE_OK );
  4482         -  fts5BufferZero(&pIter->poslist);
  4483   4482     fts5MultiIterNext(pIter->pIndex, pIter->pMulti, 0, 0);
  4484   4483     return fts5IndexReturn(pIter->pIndex);
  4485   4484   }
  4486   4485   
  4487   4486   /*
  4488   4487   ** Move to the next matching term/rowid. Used by the fts5vocab module.
  4489   4488   */
................................................................................
  4490   4489   int sqlite3Fts5IterNextScan(Fts5IndexIter *pIter){
  4491   4490     Fts5Index *p = pIter->pIndex;
  4492   4491     Fts5MultiSegIter *pMulti = pIter->pMulti;
  4493   4492   
  4494   4493     assert( pIter->pIndex->rc==SQLITE_OK );
  4495   4494     assert( pMulti );
  4496   4495   
  4497         -  fts5BufferZero(&pIter->poslist);
  4498   4496     fts5MultiIterNext(p, pMulti, 0, 0);
  4499   4497     if( p->rc==SQLITE_OK ){
  4500   4498       Fts5SegIter *pSeg = &pMulti->aSeg[ pMulti->aFirst[1].iFirst ];
  4501   4499       if( pSeg->pLeaf && pSeg->term.p[0]!=FTS5_MAIN_PREFIX ){
  4502   4500         fts5DataRelease(pSeg->pLeaf);
  4503   4501         pSeg->pLeaf = 0;
  4504   4502       }

Changes to ext/fts5/fts5parse.y.

    91     91   exprlist(A) ::= exprlist(X) cnearset(Y). {
    92     92     A = sqlite3Fts5ParseNode(pParse, FTS5_AND, X, Y, 0);
    93     93   }
    94     94   
    95     95   cnearset(A) ::= nearset(X). { 
    96     96     A = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, X); 
    97     97   }
    98         -cnearset(A) ::= STRING(X) COLON nearset(Y). { 
    99         -  sqlite3Fts5ParseSetColumn(pParse, Y, &X);
           98  +cnearset(A) ::= colset(X) COLON nearset(Y). { 
           99  +  sqlite3Fts5ParseSetColset(pParse, Y, X);
   100    100     A = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, Y); 
   101    101   }
          102  +
          103  +%type colset {Fts5ExprColset*}
          104  +%destructor colset { sqlite3_free($$); }
          105  +%type colsetlist {Fts5ExprColset*}
          106  +%destructor colsetlist { sqlite3_free($$); }
          107  +
          108  +colset(A) ::= LSP colsetlist(X) RSP. { A = X; }
          109  +colset(A) ::= STRING(X). {
          110  +  A = sqlite3Fts5ParseColset(pParse, 0, &X);
          111  +}
          112  +
          113  +colsetlist(A) ::= colsetlist(Y) STRING(X). { 
          114  +  A = sqlite3Fts5ParseColset(pParse, Y, &X); }
          115  +colsetlist(A) ::= STRING(X). { 
          116  +  A = sqlite3Fts5ParseColset(pParse, 0, &X); 
          117  +}
          118  +
   102    119   
   103    120   %type nearset     {Fts5ExprNearset*}
   104    121   %type nearphrases {Fts5ExprNearset*}
   105    122   %destructor nearset { sqlite3Fts5ParseNearsetFree($$); }
   106    123   %destructor nearphrases { sqlite3Fts5ParseNearsetFree($$); }
   107    124   
   108    125   nearset(A) ::= phrase(X). { A = sqlite3Fts5ParseNearset(pParse, 0, X); }

Changes to ext/fts5/test/fts5ac.test.

   121    121       95  {h b n j t k i h o q u}             {w n g i t o k c a m y p f l x c p}
   122    122       96  {f c x p y r b m o l m o a}         {p c a q s u n n x d c f a o}
   123    123       97  {u h h k m n k}                     {u b v n u a o c}
   124    124       98  {s p e t c z d f n w f}             {l s f j b l c e s h}
   125    125       99  {r c v w i v h a t a c v c r e}     {h h u m g o f b a e o}
   126    126   }
   127    127   
          128  +#-------------------------------------------------------------------------
   128    129   # Usage:
   129    130   #
   130    131   #   poslist aCol ?-pc VARNAME? ?-near N? ?-col C? -- phrase1 phrase2...
   131    132   #
          133  +# This command is used to test if a document (set of column values) matches
          134  +# the logical equivalent of a single FTS5 NEAR() clump and, if so, return
          135  +# the equivalent of an FTS5 position list.
          136  +#
          137  +# Parameter $aCol is passed a list of the column values for the document
          138  +# to test. Parameters $phrase1 and so on are the phrases.
          139  +#
          140  +# The result is a list of phrase hits. Each phrase hit is formatted as
          141  +# three integers separated by "." characters, in the following format:
          142  +#
          143  +#   <phrase number> . <column number> . <token offset>
          144  +#
          145  +# Options:
          146  +#
          147  +#   -near N        (NEAR distance. Default 10)
          148  +#   -col  C        (List of column indexes to match against)
          149  +#   -pc   VARNAME  (variable in caller frame to use for phrase numbering)
          150  +#
   132    151   proc poslist {aCol args} {
   133    152     set O(-near) 10
   134         -  set O(-col)  -1
          153  +  set O(-col)  {}
   135    154     set O(-pc)   ""
   136    155   
   137    156     set nOpt [lsearch -exact $args --]
   138    157     if {$nOpt<0} { error "no -- option" }
   139    158   
   140    159     foreach {k v} [lrange $args 0 [expr $nOpt-1]] {
   141    160       if {[info exists O($k)]==0} { error "unrecognized option $k" }
................................................................................
   157    176         set A($j,$i) [list]
   158    177       }
   159    178     }
   160    179   
   161    180     set iCol -1
   162    181     foreach col $aCol {
   163    182       incr iCol
   164         -    if {$O(-col)>=0 && $O(-col)!=$iCol} continue
   165         -
          183  +    if {$O(-col)!="" && [lsearch $O(-col) $iCol]<0} continue
   166    184       set nToken [llength $col]
   167    185   
   168    186       set iFL [expr $O(-near) >= $nToken ? $nToken - 1 : $O(-near)]
   169    187       for { } {$iFL < $nToken} {incr iFL} {
   170    188         for {set iPhrase 0} {$iPhrase<$nPhrase} {incr iPhrase} {
   171    189           set B($iPhrase) [list]
   172    190         }
................................................................................
   357    375       } $res
   358    376     }
   359    377   
   360    378     #-------------------------------------------------------------------------
   361    379     # Queries on a specific column.
   362    380     #
   363    381     foreach {tn expr} {
   364         -    1 "x:a"
   365         -    2 "y:a"
   366         -    3 "x:b"
   367         -    4 "y:b"
          382  +    1.1 "x:a"
          383  +    1.2 "y:a"
          384  +    1.3 "x:b"
          385  +    1.4 "y:b"
          386  +    2.1 "[x]:a"
          387  +    2.2 "[y]:a"
          388  +    2.3 "[x]:b"
          389  +    2.4 "[y]:b"
          390  +
          391  +    3.1 "[x y]:a"
          392  +    3.2 "[y x]:a"
          393  +    3.3 "[x x]:b"
          394  +    3.4 "[y y]:b"
          395  +
          396  +    4.1 {["x" "y"]:a}
          397  +    4.2 {["y" x]:a}
          398  +    4.3 {[x "x"]:b}
          399  +    4.4 {["y" y]:b}
   368    400     } {
   369    401       set res [matchdata 1 $expr]
   370    402       do_execsql_test $tn2.3.$tn.[llength $res] { 
   371    403         SELECT rowid, fts5_test_poslist(xx) FROM xx WHERE xx match $expr
   372    404       } $res
   373    405     }
   374    406