Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Add a configuration option to remap the "rank" column to an auxiliary fts5 function. |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | fts5 |
Files: | files | file ages | folders |
SHA1: |
b5f5971283b9b2f60c16f9675099855a |
User & Date: | dan 2014-12-02 20:18:11.604 |
Context
2014-12-03
| ||
17:27 | Begin testing fts5 OOM and IO error handling. (check-in: 2037dba62f user: dan tags: fts5) | |
2014-12-02
| ||
20:18 | Add a configuration option to remap the "rank" column to an auxiliary fts5 function. (check-in: b5f5971283 user: dan tags: fts5) | |
2014-12-01
| ||
20:05 | Add code to parse a rank() function specification. And a tcl interface to add auxiliary functions to fts5. (check-in: 9c1697a2aa user: dan tags: fts5) | |
Changes
Changes to ext/fts5/fts5.c.
︙ | ︙ | |||
153 154 155 156 157 158 159 160 | sqlite3_vtab_cursor base; /* Base class used by SQLite core */ int idxNum; /* idxNum passed to xFilter() */ sqlite3_stmt *pStmt; /* Statement used to read %_content */ Fts5Expr *pExpr; /* Expression for MATCH queries */ Fts5Sorter *pSorter; /* Sorter for "ORDER BY rank" queries */ int csrflags; /* Mask of cursor flags (see below) */ Fts5Cursor *pNext; /* Next cursor in Fts5Cursor.pCsr list */ Fts5Auxiliary *pRank; /* Rank callback (or NULL) */ | > > > | > > | 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | sqlite3_vtab_cursor base; /* Base class used by SQLite core */ int idxNum; /* idxNum passed to xFilter() */ sqlite3_stmt *pStmt; /* Statement used to read %_content */ Fts5Expr *pExpr; /* Expression for MATCH queries */ Fts5Sorter *pSorter; /* Sorter for "ORDER BY rank" queries */ int csrflags; /* Mask of cursor flags (see below) */ Fts5Cursor *pNext; /* Next cursor in Fts5Cursor.pCsr list */ char *zSpecial; /* Result of special query */ /* "rank" function. Populated on demand from vtab.xColumn(). */ Fts5Auxiliary *pRank; /* Rank callback (or NULL) */ int nRankArg; /* Number of trailing arguments for rank() */ sqlite3_value **apRankArg; /* Array of trailing arguments */ sqlite3_stmt *pRankArgStmt; /* Origin of objects in apRankArg[] */ /* Variables used by auxiliary functions */ i64 iCsrId; /* Cursor id */ Fts5Auxiliary *pAux; /* Currently executing extension function */ Fts5Auxdata *pAuxdata; /* First in linked list of saved aux-data */ int *aColumnSize; /* Values for xColumnSize() */ int nInstCount; /* Number of phrase instances */ |
︙ | ︙ | |||
535 536 537 538 539 540 541 542 543 544 545 546 547 548 | sqlite3_free(pData); } /* Remove the cursor from the Fts5Global.pCsr list */ for(pp=&pTab->pGlobal->pCsr; (*pp)!=pCsr; pp=&(*pp)->pNext); *pp = pCsr->pNext; sqlite3_free(pCsr->zSpecial); sqlite3_free(pCsr); return SQLITE_OK; } static int fts5SorterNext(Fts5Cursor *pCsr){ Fts5Sorter *pSorter = pCsr->pSorter; | > > > | 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 | sqlite3_free(pData); } /* Remove the cursor from the Fts5Global.pCsr list */ for(pp=&pTab->pGlobal->pCsr; (*pp)!=pCsr; pp=&(*pp)->pNext); *pp = pCsr->pNext; sqlite3_finalize(pCsr->pRankArgStmt); sqlite3_free(pCsr->apRankArg); sqlite3_free(pCsr->zSpecial); sqlite3_free(pCsr); return SQLITE_OK; } static int fts5SorterNext(Fts5Cursor *pCsr){ Fts5Sorter *pSorter = pCsr->pSorter; |
︙ | ︙ | |||
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 | static int fts5CursorFirstSorted(Fts5Table *pTab, Fts5Cursor *pCsr, int bAsc){ Fts5Config *pConfig = pTab->pConfig; Fts5Sorter *pSorter; int nPhrase; int nByte; int rc = SQLITE_OK; char *zSql; nPhrase = sqlite3Fts5ExprPhraseCount(pCsr->pExpr); nByte = sizeof(Fts5Sorter) + sizeof(int) * nPhrase; pSorter = (Fts5Sorter*)sqlite3_malloc(nByte); if( pSorter==0 ) return SQLITE_NOMEM; memset(pSorter, 0, nByte); pSorter->nIdx = nPhrase; /* TODO: It would be better to have some system for reusing statement ** handles here, rather than preparing a new one for each query. But that ** is not possible as SQLite reference counts the virtual table objects. ** And since the statement required here reads from this very virtual ** table, saving it creates a circular reference. ** ** If SQLite a built-in statement cache, this wouldn't be a problem. */ | > | | > > | 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | static int fts5CursorFirstSorted(Fts5Table *pTab, Fts5Cursor *pCsr, int bAsc){ Fts5Config *pConfig = pTab->pConfig; Fts5Sorter *pSorter; int nPhrase; int nByte; int rc = SQLITE_OK; char *zSql; const char *zRank = pConfig->zRank ? pConfig->zRank : FTS5_DEFAULT_RANK; nPhrase = sqlite3Fts5ExprPhraseCount(pCsr->pExpr); nByte = sizeof(Fts5Sorter) + sizeof(int) * nPhrase; pSorter = (Fts5Sorter*)sqlite3_malloc(nByte); if( pSorter==0 ) return SQLITE_NOMEM; memset(pSorter, 0, nByte); pSorter->nIdx = nPhrase; /* TODO: It would be better to have some system for reusing statement ** handles here, rather than preparing a new one for each query. But that ** is not possible as SQLite reference counts the virtual table objects. ** And since the statement required here reads from this very virtual ** table, saving it creates a circular reference. ** ** If SQLite a built-in statement cache, this wouldn't be a problem. */ zSql = sqlite3_mprintf("SELECT rowid, rank FROM %Q.%Q ORDER BY %s(%s%s%s) %s", pConfig->zDb, pConfig->zName, zRank, pConfig->zName, (pConfig->zRankArgs ? ", " : ""), (pConfig->zRankArgs ? pConfig->zRankArgs : ""), bAsc ? "ASC" : "DESC" ); if( zSql==0 ){ rc = SQLITE_NOMEM; }else{ rc = sqlite3_prepare_v2(pConfig->db, zSql, -1, &pSorter->pStmt, 0); sqlite3_free(zSql); |
︙ | ︙ | |||
717 718 719 720 721 722 723 724 725 726 727 728 729 730 | pTab->base.zErrMsg = sqlite3_mprintf("unknown special query: %.*s", n, z); rc = SQLITE_ERROR; } return rc; } /* ** This is the xFilter interface for the virtual table. See ** the virtual table xFilter method documentation for additional ** information. */ static int fts5FilterMethod( | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 | pTab->base.zErrMsg = sqlite3_mprintf("unknown special query: %.*s", n, z); rc = SQLITE_ERROR; } return rc; } /* ** Search for an auxiliary function named zName that can be used with table ** pTab. If one is found, return a pointer to the corresponding Fts5Auxiliary ** structure. Otherwise, if no such function exists, return NULL. */ static Fts5Auxiliary *fts5FindAuxiliary(Fts5Table *pTab, const char *zName){ Fts5Auxiliary *pAux; for(pAux=pTab->pGlobal->pAux; pAux; pAux=pAux->pNext){ if( sqlite3_stricmp(zName, pAux->zFunc)==0 ) return pAux; } /* No function of the specified name was found. Return 0. */ return 0; } static int fts5FindRankFunction(Fts5Cursor *pCsr){ Fts5Table *pTab = (Fts5Table*)(pCsr->base.pVtab); Fts5Config *pConfig = pTab->pConfig; const char *zRank = pConfig->zRank; int rc = SQLITE_OK; Fts5Auxiliary *pAux; if( zRank==0 ) zRank = FTS5_DEFAULT_RANK; if( pTab->pConfig->zRankArgs ){ char *zSql = sqlite3_mprintf("SELECT %s", pTab->pConfig->zRankArgs); if( zSql==0 ){ rc = SQLITE_NOMEM; }else{ sqlite3_stmt *pStmt = 0; rc = sqlite3_prepare_v2(pConfig->db, zSql, -1, &pStmt, 0); sqlite3_free(zSql); assert( rc==SQLITE_OK || pCsr->pRankArgStmt==0 ); if( rc==SQLITE_OK ){ if( SQLITE_ROW==sqlite3_step(pStmt) ){ int nByte; pCsr->nRankArg = sqlite3_column_count(pStmt); nByte = sizeof(sqlite3_value*)*pCsr->nRankArg; pCsr->apRankArg = (sqlite3_value**)sqlite3Fts5MallocZero(&rc, nByte); if( rc==SQLITE_OK ){ int i; for(i=0; i<pCsr->nRankArg; i++){ pCsr->apRankArg[i] = sqlite3_column_value(pStmt, i); } } pCsr->pRankArgStmt = pStmt; }else{ rc = sqlite3_finalize(pStmt); assert( rc!=SQLITE_OK ); } } } } if( rc==SQLITE_OK ){ pAux = fts5FindAuxiliary(pTab, zRank); if( pAux==0 ){ assert( pTab->base.zErrMsg==0 ); pTab->base.zErrMsg = sqlite3_mprintf("no such function: %s", zRank); rc = SQLITE_ERROR; } } pCsr->pRank = pAux; return rc; } /* ** This is the xFilter interface for the virtual table. See ** the virtual table xFilter method documentation for additional ** information. */ static int fts5FilterMethod( |
︙ | ︙ | |||
749 750 751 752 753 754 755 | ** processing for a "... MATCH <expr> ORDER BY rank" query (ePlan is ** set to FTS5_PLAN_SORTED_MATCH). pSortCsr is the cursor that will ** return results to the user for this query. The current cursor ** (pCursor) is used to execute the query issued by function ** fts5CursorFirstSorted() above. */ assert( FTS5_PLAN(idxNum)==FTS5_PLAN_SCAN ); pCsr->idxNum = FTS5_PLAN_SOURCE; | < < | 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 | ** processing for a "... MATCH <expr> ORDER BY rank" query (ePlan is ** set to FTS5_PLAN_SORTED_MATCH). pSortCsr is the cursor that will ** return results to the user for this query. The current cursor ** (pCursor) is used to execute the query issued by function ** fts5CursorFirstSorted() above. */ assert( FTS5_PLAN(idxNum)==FTS5_PLAN_SCAN ); pCsr->idxNum = FTS5_PLAN_SOURCE; pCsr->pExpr = pTab->pSortCsr->pExpr; rc = fts5CursorFirst(pTab, pCsr, bAsc); }else{ int ePlan = FTS5_PLAN(idxNum); pCsr->idxNum = idxNum; if( ePlan==FTS5_PLAN_MATCH || ePlan==FTS5_PLAN_SORTED_MATCH ){ const char *zExpr = (const char*)sqlite3_value_text(apVal[0]); if( zExpr[0]=='*' ){ /* The user has issued a query of the form "MATCH '*...'". This ** indicates that the MATCH expression is not a full text query, ** but a request for an internal parameter. */ rc = fts5SpecialMatch(pTab, pCsr, &zExpr[1]); }else{ char **pzErr = &pTab->base.zErrMsg; rc = sqlite3Fts5ExprNew(pTab->pConfig, zExpr, &pCsr->pExpr, pzErr); if( rc==SQLITE_OK ){ if( ePlan==FTS5_PLAN_MATCH ){ rc = fts5CursorFirst(pTab, pCsr, bAsc); }else{ rc = fts5CursorFirstSorted(pTab, pCsr, bAsc); } |
︙ | ︙ | |||
1088 1089 1090 1091 1092 1093 1094 | int nByte; nIter = sqlite3Fts5ExprPhraseCount(pCsr->pExpr); nByte = sizeof(Fts5PoslistReader) * nIter; aIter = (Fts5PoslistReader*)sqlite3Fts5MallocZero(&rc, nByte); if( aIter ){ Fts5Buffer buf = {0, 0, 0}; /* Build up aInst[] here */ | | | 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 | int nByte; nIter = sqlite3Fts5ExprPhraseCount(pCsr->pExpr); nByte = sizeof(Fts5PoslistReader) * nIter; aIter = (Fts5PoslistReader*)sqlite3Fts5MallocZero(&rc, nByte); if( aIter ){ Fts5Buffer buf = {0, 0, 0}; /* Build up aInst[] here */ int nInst = 0; /* Number instances seen so far */ int i; /* Initialize all iterators */ for(i=0; i<nIter; i++){ const u8 *a; int n = fts5CsrPoslist(pCsr, i, &a); sqlite3Fts5PoslistReaderInit(-1, a, n, &aIter[i]); |
︙ | ︙ | |||
1422 1423 1424 1425 1426 1427 1428 | if( pCsr->idxNum==FTS5_PLAN_SPECIAL ){ if( iCol==pConfig->nCol ){ sqlite3_result_text(pCtx, pCsr->zSpecial, -1, SQLITE_TRANSIENT); } }else if( iCol==pConfig->nCol ){ | < < < | | | | | < > > > > > > > | | > | 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 | if( pCsr->idxNum==FTS5_PLAN_SPECIAL ){ if( iCol==pConfig->nCol ){ sqlite3_result_text(pCtx, pCsr->zSpecial, -1, SQLITE_TRANSIENT); } }else if( iCol==pConfig->nCol ){ /* User is requesting the value of the special column with the same name ** as the table. Return the cursor integer id number. This value is only ** useful in that it may be passed as the first argument to an FTS5 ** auxiliary function. */ sqlite3_result_int64(pCtx, pCsr->iCsrId); }else if( iCol==pConfig->nCol+1 ){ /* The value of the "rank" column. */ if( FTS5_PLAN(pCsr->idxNum)==FTS5_PLAN_SOURCE ){ fts5PoslistBlob(pCtx, pCsr); }else if( FTS5_PLAN(pCsr->idxNum)==FTS5_PLAN_MATCH || FTS5_PLAN(pCsr->idxNum)==FTS5_PLAN_SORTED_MATCH ){ if( pCsr->pRank || SQLITE_OK==(rc = fts5FindRankFunction(pCsr)) ){ fts5ApiInvoke(pCsr->pRank, pCsr, pCtx, pCsr->nRankArg, pCsr->apRankArg); } } }else{ rc = fts5SeekCursor(pCsr); if( rc==SQLITE_OK ){ sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pStmt, iCol+1)); } } |
︙ | ︙ | |||
1460 1461 1462 1463 1464 1465 1466 | const char *zName, /* Name of SQL function */ void (**pxFunc)(sqlite3_context*,int,sqlite3_value**), /* OUT: Result */ void **ppArg /* OUT: User data for *pxFunc */ ){ Fts5Table *pTab = (Fts5Table*)pVtab; Fts5Auxiliary *pAux; | | | | | | < | 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 | const char *zName, /* Name of SQL function */ void (**pxFunc)(sqlite3_context*,int,sqlite3_value**), /* OUT: Result */ void **ppArg /* OUT: User data for *pxFunc */ ){ Fts5Table *pTab = (Fts5Table*)pVtab; Fts5Auxiliary *pAux; pAux = fts5FindAuxiliary(pTab, zName); if( pAux ){ *pxFunc = fts5ApiCallback; *ppArg = (void*)pAux; return 1; } /* No function of the specified name was found. Return 0. */ return 0; } /* |
︙ | ︙ |
Changes to ext/fts5/fts5Int.h.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 | ** Maximum number of prefix indexes on single FTS5 table. This must be ** less than 32. If it is set to anything large than that, an #error ** directive in fts5_index.c will cause the build to fail. */ #define FTS5_MAX_PREFIX_INDEXES 31 #define FTS5_DEFAULT_NEARDIST 10 /* Name of rank column */ #define FTS5_RANK_NAME "rank" /************************************************************************** ** Interface to code in fts5.c. */ | > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | ** Maximum number of prefix indexes on single FTS5 table. This must be ** less than 32. If it is set to anything large than that, an #error ** directive in fts5_index.c will cause the build to fail. */ #define FTS5_MAX_PREFIX_INDEXES 31 #define FTS5_DEFAULT_NEARDIST 10 #define FTS5_DEFAULT_RANK "bm25" /* Name of rank column */ #define FTS5_RANK_NAME "rank" /************************************************************************** ** Interface to code in fts5.c. */ |
︙ | ︙ |
Changes to ext/fts5/fts5_config.c.
︙ | ︙ | |||
464 465 466 467 468 469 470 | if( rc==SQLITE_OK ){ p = fts5ConfigSkipWhitespace(p); if( *p!='(' ) rc = SQLITE_ERROR; p++; } if( rc==SQLITE_OK ){ | | > > | | 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | 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; 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); |
︙ | ︙ |
Changes to test/fts5ae.test.
︙ | ︙ | |||
269 270 271 272 273 274 275 | SELECT rowid FROM t8 WHERE t8 MATCH $q ORDER BY +rank DESC; } $res do_execsql_test 8.2.$tn.3 { SELECT rowid FROM t8 WHERE t8 MATCH $q ORDER BY rank DESC; } $res } | < | 269 270 271 272 273 274 275 276 277 278 | SELECT rowid FROM t8 WHERE t8 MATCH $q ORDER BY +rank DESC; } $res do_execsql_test 8.2.$tn.3 { SELECT rowid FROM t8 WHERE t8 MATCH $q ORDER BY rank DESC; } $res } finish_test |
Changes to test/fts5al.test.
︙ | ︙ | |||
76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } { do_test 2.2.$tn { catchsql { INSERT INTO ft1(ft1, rank) VALUES('rank', $defn) } } {1 {SQL logic error or missing database}} } #------------------------------------------------------------------------- # do_execsql_test 3.1 { CREATE VIRTUAL TABLE t1 USING fts5(x); INSERT INTO t1 VALUES('q w e r t y'); INSERT INTO t1 VALUES('y t r e w q'); } | > | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | } { do_test 2.2.$tn { catchsql { INSERT INTO ft1(ft1, rank) VALUES('rank', $defn) } } {1 {SQL logic error or missing database}} } #------------------------------------------------------------------------- # Assorted tests of the tcl interface for creating extension functions. # do_execsql_test 3.1 { CREATE VIRTUAL TABLE t1 USING fts5(x); INSERT INTO t1 VALUES('q w e r t y'); INSERT INTO t1 VALUES('y t r e w q'); } |
︙ | ︙ | |||
130 131 132 133 134 135 136 | } proc coltest {cmd} { list [$cmd xColumnSize 0] [$cmd xColumnText 0] } sqlite3_fts5_create_function db coltest coltest | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | } proc coltest {cmd} { list [$cmd xColumnSize 0] [$cmd xColumnText 0] } sqlite3_fts5_create_function db coltest coltest do_execsql_test 3.5.1 { SELECT coltest(t1) FROM t1 WHERE t1 MATCH 'q' } { {6 {y t r e w q}} {6 {q w e r t y}} } #------------------------------------------------------------------------- # Tests for remapping the "rank" column. # # 4.1.*: Mapped to a function with no arguments. # 4.2.*: Mapped to a function with one or more arguments. # do_execsql_test 4.0 { CREATE VIRTUAL TABLE t2 USING fts5(a, b); INSERT INTO t2 VALUES('a s h g s b j m r h', 's b p a d b b a o e'); INSERT INTO t2 VALUES('r h n t a g r d d i', 'l d n j r c f t o q'); INSERT INTO t2 VALUES('q k n i k c a a e m', 'c h n j p g s c i t'); INSERT INTO t2 VALUES('h j g t r e l s g s', 'k q k c i i c k n s'); INSERT INTO t2 VALUES('b l k h d n n n m i', 'p t i a r b t q o l'); INSERT INTO t2 VALUES('k r i l j b g i p a', 't q c h a i m g n l'); INSERT INTO t2 VALUES('a e c q n m o m d g', 'l c t g i s q g q e'); INSERT INTO t2 VALUES('b o j h f o g b p e', 'r t l h s b g i c p'); INSERT INTO t2 VALUES('s q k f q b j g h f', 'n m a o p e i e k t'); INSERT INTO t2 VALUES('o q g g q c o k a b', 'r t k p t f t h p c'); } proc firstinst {cmd} { foreach {p c o} [$cmd xInst 0] {} expr $c*100 + $o } sqlite3_fts5_create_function db firstinst firstinst do_execsql_test 4.1.1 { SELECT rowid, firstinst(t2) FROM t2 WHERE t2 MATCH 'a' ORDER BY rowid ASC } { 1 0 2 4 3 6 5 103 6 9 7 0 9 102 10 8 } do_execsql_test 4.1.2 { INSERT INTO t2(t2, rank) VALUES('rank', 'firstinst()'); SELECT rowid, rank FROM t2 WHERE t2 MATCH 'a' ORDER BY rowid ASC } { 1 0 2 4 3 6 5 103 6 9 7 0 9 102 10 8 } do_execsql_test 4.1.3 { SELECT rowid, rank FROM t2 WHERE t2 MATCH 'a' ORDER BY rank DESC } { 5 103 9 102 6 9 10 8 3 6 2 4 7 0 1 0 } do_execsql_test 4.1.4 { INSERT INTO t2(t2, rank) VALUES('rank', 'firstinst ( ) '); SELECT rowid, rank FROM t2 WHERE t2 MATCH 'a' ORDER BY rank DESC } { 5 103 9 102 6 9 10 8 3 6 2 4 7 0 1 0 } proc rowidplus {cmd ival} { expr [$cmd xRowid] + $ival } sqlite3_fts5_create_function db rowidplus rowidplus do_execsql_test 4.2.1 { INSERT INTO t2(t2, rank) VALUES('rank', 'rowidplus(100) '); SELECT rowid, rank FROM t2 WHERE t2 MATCH 'o + q + g' } { 10 110 } do_execsql_test 4.2.2 { INSERT INTO t2(t2, rank) VALUES('rank', 'rowidplus(111) '); SELECT rowid, rank FROM t2 WHERE t2 MATCH 'o + q + g' } { 10 121 } finish_test |