Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Update the spellfix virtual table to the latest development code. |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
6954fef006431d153de6e63e362b8d26 |
User & Date: | drh 2012-08-14 17:29:27.517 |
Context
2012-08-14
| ||
18:43 | Add an assert() to the btree rebalancer in order to silence a clang/scan-build warning. (check-in: 6730579cf5 user: drh tags: trunk) | |
17:29 | Update the spellfix virtual table to the latest development code. (check-in: 6954fef006 user: drh tags: trunk) | |
01:45 | Refer to the file mapping Win32 API functions only when absolutely necessary. (check-in: 1de2237d00 user: mistachkin tags: trunk) | |
Changes
Changes to src/test_spellfix.c.
1 2 3 4 5 6 7 8 9 10 11 12 | /* ** 2012 April 10 ** ** 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. ** ************************************************************************* ** | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /* ** 2012 April 10 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ************************************************************************* ** ** This module implements the spellfix1 VIRTUAL TABLE that can be used ** to search a large vocabulary for close matches. See separate ** documentation files (spellfix1.wiki and editdist3.wiki) for details. */ #if SQLITE_CORE # include "sqliteInt.h" #else # include <string.h> # include <stdio.h> # include <stdlib.h> |
︙ | ︙ | |||
302 303 304 305 306 307 308 | ** 4 'D' Alveolar stops: D T ** 5 'H' Letter H at the beginning of a word ** 6 'L' Glide: L ** 7 'R' Semivowel: R ** 8 'M' Nasals: M N ** 9 'W' Letter W at the beginning of a word ** 10 'Y' Letter Y at the beginning of a word. | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | ** 4 'D' Alveolar stops: D T ** 5 'H' Letter H at the beginning of a word ** 6 'L' Glide: L ** 7 'R' Semivowel: R ** 8 'M' Nasals: M N ** 9 'W' Letter W at the beginning of a word ** 10 'Y' Letter Y at the beginning of a word. ** 11 '9' Digits: 0 1 2 3 4 5 6 7 8 9 ** 12 ' ' White space ** 13 '?' Other. */ #define CCLASS_SILENT 0 #define CCLASS_VOWEL 1 #define CCLASS_B 2 #define CCLASS_C 3 |
︙ | ︙ | |||
462 463 464 465 466 467 468 | case 'g': case 'k': { if( zIn[1]=='n' ){ zIn++; nIn--; } break; } } } | < | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | case 'g': case 'k': { if( zIn[1]=='n' ){ zIn++; nIn--; } break; } } } for(i=0; i<nIn; i++){ unsigned char c = zIn[i]; if( i+1<nIn ){ if( c=='w' && zIn[i+1]=='r' ) continue; if( c=='d' && (zIn[i+1]=='j' || zIn[i+1]=='g') ) continue; if( i+2<nIn ){ if( c=='t' && zIn[i+1]=='c' && zIn[i+2]=='h' ) continue; |
︙ | ︙ | |||
582 583 584 585 586 587 588 | /* differ only in case */ return 0; } classFrom = characterClass(cPrev, cFrom); classTo = characterClass(cPrev, cTo); if( classFrom==classTo ){ /* Same character class */ | | | 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | /* differ only in case */ return 0; } classFrom = characterClass(cPrev, cFrom); classTo = characterClass(cPrev, cTo); if( classFrom==classTo ){ /* Same character class */ return 40; } if( classFrom>=CCLASS_B && classFrom<=CCLASS_Y && classTo>=CCLASS_B && classTo<=CCLASS_Y ){ /* Convert from one consonant to another, but in a different class */ return 75; } /* Any other subsitution */ |
︙ | ︙ | |||
612 613 614 615 616 617 618 | ** ** If pnMatch is not NULL, then *pnMatch is set to the number of bytes ** of zB that matched the pattern in zA. If zA does not end with a '*', ** then this value is always the number of bytes in zB (i.e. strlen(zB)). ** If zA does end in a '*', then it is the number of bytes in the prefix ** of zB that was deemed to match zA. */ | | | 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | ** ** If pnMatch is not NULL, then *pnMatch is set to the number of bytes ** of zB that matched the pattern in zA. If zA does not end with a '*', ** then this value is always the number of bytes in zB (i.e. strlen(zB)). ** If zA does end in a '*', then it is the number of bytes in the prefix ** of zB that was deemed to match zA. */ static int editdist1(const char *zA, const char *zB, int *pnMatch){ int nA, nB; /* Number of characters in zA[] and zB[] */ int xA, xB; /* Loop counters for zA[] and zB[] */ char cA, cB; /* Current character of zA and zB */ char cAprev, cBprev; /* Previous character of zA and zB */ char cAnext, cBnext; /* Next character in zA and zB */ int d; /* North-west cost value */ int dc = 0; /* North-west character value */ |
︙ | ︙ | |||
641 642 643 644 645 646 647 | #if 0 printf("A=\"%s\" B=\"%s\" dc=%c\n", zA, zB, dc?dc:' '); #endif /* Verify input strings and measure their lengths */ for(nA=0; zA[nA]; nA++){ | | | | 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | #if 0 printf("A=\"%s\" B=\"%s\" dc=%c\n", zA, zB, dc?dc:' '); #endif /* Verify input strings and measure their lengths */ for(nA=0; zA[nA]; nA++){ if( zA[nA]&0x80 ) return -2; } for(nB=0; zB[nB]; nB++){ if( zB[nB]&0x80 ) return -2; } /* Special processing if either string is empty */ if( nA==0 ){ cBprev = dc; for(xB=res=0; (cB = zB[xB])!=0; xB++){ res += insertOrDeleteCost(cBprev, cB, zB[xB+1])/FINAL_INS_COST_DIV; |
︙ | ︙ | |||
752 753 754 755 756 757 758 | if( m[xB]<res ){ res = m[xB]; if( pnMatch ) *pnMatch = xB+nMatch; } } }else{ res = m[nB]; | > > | < < | | 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 | if( m[xB]<res ){ res = m[xB]; if( pnMatch ) *pnMatch = xB+nMatch; } } }else{ res = m[nB]; /* In the current implementation, pnMatch is always NULL if zA does ** not end in "*" */ assert( pnMatch==0 ); } sqlite3_free(toFree); return res; } /* ** Function: editdist(A,B) ** ** Return the cost of transforming string A into string B. Both strings ** must be pure ASCII text. If A ends with '*' then it is assumed to be ** a prefix of B and extra characters on the end of B have minimal additional ** cost. */ static void editdistSqlFunc( sqlite3_context *context, int argc, sqlite3_value **argv ){ int res = editdist1( (const char*)sqlite3_value_text(argv[0]), (const char*)sqlite3_value_text(argv[1]), 0); if( res<0 ){ if( res==(-3) ){ sqlite3_result_error_nomem(context); }else if( res==(-2) ){ sqlite3_result_error(context, "non-ASCII input to editdist()", -1); }else{ sqlite3_result_error(context, "NULL input to editdist()", -1); |
︙ | ︙ | |||
920 921 922 923 924 925 926 | */ static int editDist3ConfigLoad( EditDist3Config *p, /* The edit distance configuration to load */ sqlite3 *db, /* Load from this database */ const char *zTable /* Name of the table from which to load */ ){ sqlite3_stmt *pStmt; | | | | > > | < | | > | | 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 | */ static int editDist3ConfigLoad( EditDist3Config *p, /* The edit distance configuration to load */ sqlite3 *db, /* Load from this database */ const char *zTable /* Name of the table from which to load */ ){ sqlite3_stmt *pStmt; int rc, rc2; char *zSql; int iLangPrev = -9999; EditDist3Lang *pLang; zSql = sqlite3_mprintf("SELECT iLang, cFrom, cTo, iCost" " FROM \"%w\" WHERE iLang>=0 ORDER BY iLang", zTable); if( zSql==0 ) return SQLITE_NOMEM; rc = sqlite3_prepare(db, zSql, -1, &pStmt, 0); sqlite3_free(zSql); if( rc ) return rc; editDist3ConfigClear(p); while( sqlite3_step(pStmt)==SQLITE_ROW ){ int iLang = sqlite3_column_int(pStmt, 0); const char *zFrom = (const char*)sqlite3_column_text(pStmt, 1); int nFrom = zFrom ? sqlite3_column_bytes(pStmt, 1) : 0; const char *zTo = (const char*)sqlite3_column_text(pStmt, 2); int nTo = zTo ? sqlite3_column_bytes(pStmt, 2) : 0; int iCost = sqlite3_column_int(pStmt, 3); assert( zFrom!=0 || nFrom==0 ); assert( zTo!=0 || nTo==0 ); if( nFrom>100 || nTo>100 ) continue; if( iCost<0 ) continue; if( iLang!=iLangPrev ){ EditDist3Lang *pNew; pNew = sqlite3_realloc(p->a, (p->nLang+1)*sizeof(p->a[0])); if( pNew==0 ){ rc = SQLITE_NOMEM; break; } p->a = pNew; pLang = &p->a[p->nLang]; p->nLang++; pLang->iLang = iLang; pLang->iInsCost = 100; pLang->iDelCost = 100; pLang->iSubCost = 150; pLang->pCost = 0; iLangPrev = iLang; } if( nFrom==1 && zFrom[0]=='?' && nTo==0 ){ pLang->iDelCost = iCost; }else if( nFrom==0 && nTo==1 && zTo[0]=='?' ){ pLang->iInsCost = iCost; |
︙ | ︙ | |||
977 978 979 980 981 982 983 | pCost->iCost = iCost; memcpy(pCost->a, zFrom, nFrom); memcpy(pCost->a + nFrom, zTo, nTo); pCost->pNext = pLang->pCost; pLang->pCost = pCost; } } | | > | 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 | pCost->iCost = iCost; memcpy(pCost->a, zFrom, nFrom); memcpy(pCost->a + nFrom, zTo, nTo); pCost->pNext = pLang->pCost; pLang->pCost = pCost; } } rc2 = sqlite3_finalize(pStmt); if( rc==SQLITE_OK ) rc = rc2; return rc; } /* ** Return the length (in bytes) of a utf-8 character. Or return a maximum ** of N. */ |
︙ | ︙ | |||
1015 1016 1017 1018 1019 1020 1021 | } /* ** Return TRUE (non-zero) of the To side of the given cost matches ** the given string. */ static int matchFrom(EditDist3Cost *p, const char *z, int n){ | | | 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 | } /* ** Return TRUE (non-zero) of the To side of the given cost matches ** the given string. */ static int matchFrom(EditDist3Cost *p, const char *z, int n){ assert( p->nFrom<=n ); if( memcmp(p->a, z, p->nFrom)!=0 ) return 0; return 1; } /* ** Return TRUE (non-zero) of the next FROM character and the next TO ** character are the same. |
︙ | ︙ | |||
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 | const char *z, int n ){ EditDist3FromString *pStr; EditDist3Cost *p; int i; if( n<0 ) n = (int)strlen(z); pStr = sqlite3_malloc( sizeof(*pStr) + sizeof(pStr->a[0])*n + n + 1 ); if( pStr==0 ) return 0; pStr->a = (EditDist3From*)&pStr[1]; pStr->n = n; pStr->z = (char*)&pStr->a[n]; memcpy(pStr->z, z, n+1); if( n && z[n-1]=='*' ){ pStr->isPrefix = 1; n--; pStr->n--; | > > | 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 | const char *z, int n ){ EditDist3FromString *pStr; EditDist3Cost *p; int i; if( z==0 ) return 0; if( n<0 ) n = (int)strlen(z); pStr = sqlite3_malloc( sizeof(*pStr) + sizeof(pStr->a[0])*n + n + 1 ); if( pStr==0 ) return 0; pStr->a = (EditDist3From*)&pStr[1]; memset(pStr->a, 0, sizeof(pStr->a[0])*n); pStr->n = n; pStr->z = (char*)&pStr->a[n]; memcpy(pStr->z, z, n+1); if( n && z[n-1]=='*' ){ pStr->isPrefix = 1; n--; pStr->n--; |
︙ | ︙ | |||
1109 1110 1111 1112 1113 1114 1115 | pStr = 0; break; } } return pStr; } | < < < < < < < < < < < < < < < < < < < < < < < < | 846 847 848 849 850 851 852 853 854 855 856 857 858 859 | pStr = 0; break; } } return pStr; } /* ** Update entry m[i] such that it is the minimum of its current value ** and m[j]+iCost. ** ** If the iCost is 1,000,000 or greater, then consider the cost to be ** infinite and skip the update. */ |
︙ | ︙ | |||
1181 1182 1183 1184 1185 1186 1187 | EditDist3FromString f = *pFrom; EditDist3To *a2; unsigned int *m; int szRow; EditDist3Cost *p; int res; | < < < < < < < < < < < < < < < < | 894 895 896 897 898 899 900 901 902 903 904 905 906 907 | EditDist3FromString f = *pFrom; EditDist3To *a2; unsigned int *m; int szRow; EditDist3Cost *p; int res; /* allocate the Wagner matrix and the aTo[] array for the TO string */ n = (f.n+1)*(n2+1); n = (n+1)&~1; m = sqlite3_malloc( n*sizeof(m[0]) + sizeof(a2[0])*n2 ); if( m==0 ) return -1; /* Out of memory */ a2 = (EditDist3To*)&m[n]; memset(a2, 0, sizeof(a2[0])*n2); |
︙ | ︙ | |||
1280 1281 1282 1283 1284 1285 1286 | if( matchTo(p, z2+i2, n2-i2) ){ updateCost(m, cxd+p->nFrom+szRow*p->nTo, cxd, p->iCost); } } } } | | | 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 | if( matchTo(p, z2+i2, n2-i2) ){ updateCost(m, cxd+p->nFrom+szRow*p->nTo, cxd, p->iCost); } } } } #if 0 /* Enable for debugging */ printf(" ^"); for(i1=0; i1<f.n; i1++) printf(" %c-%2x", f.z[i1], f.z[i1]&0xff); printf("\n ^:"); for(i1=0; i1<szRow; i1++){ int v = m[i1]; if( v>9999 ) printf(" ****"); else printf(" %4d", v); |
︙ | ︙ | |||
1379 1380 1381 1382 1383 1384 1385 | pFrom = editDist3FromStringNew(pLang, zA, nA); if( pFrom==0 ){ sqlite3_result_error_nomem(context); return; } dist = editDist3Core(pFrom, zB, nB, pLang, 0); editDist3FromStringDelete(pFrom); | > > > | > | 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 | pFrom = editDist3FromStringNew(pLang, zA, nA); if( pFrom==0 ){ sqlite3_result_error_nomem(context); return; } dist = editDist3Core(pFrom, zB, nB, pLang, 0); editDist3FromStringDelete(pFrom); if( dist==(-1) ){ sqlite3_result_error_nomem(context); }else{ sqlite3_result_int(context, dist); } } } /* ** Register the editDist3 function with SQLite */ static int editDist3Install(sqlite3 *db){ |
︙ | ︙ | |||
1435 1436 1437 1438 1439 1440 1441 | /* ** Return the value of the first UTF-8 character in the string. */ static int utf8Read(const unsigned char *z, int n, int *pSize){ int c, i; | > > | | 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 | /* ** Return the value of the first UTF-8 character in the string. */ static int utf8Read(const unsigned char *z, int n, int *pSize){ int c, i; /* All callers to this routine (in the current implementation) ** always have n>0. */ if( NEVER(n==0) ){ c = i = 0; }else{ c = z[0]; i = 1; if( c>=0xc0 ){ c = sqlite3Utf8Trans1[c-0xc0]; while( i<n && (z[i] & 0xc0)==0x80 ){ |
︙ | ︙ | |||
1876 1877 1878 1879 1880 1881 1882 | ** The returned string might contain more characters than the input. ** ** Space to hold the returned string comes from sqlite3_malloc() and ** should be freed by the caller. */ static unsigned char *transliterate(const unsigned char *zIn, int nIn){ unsigned char *zOut = sqlite3_malloc( nIn*4 + 1 ); | | | | | 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 | ** The returned string might contain more characters than the input. ** ** Space to hold the returned string comes from sqlite3_malloc() and ** should be freed by the caller. */ static unsigned char *transliterate(const unsigned char *zIn, int nIn){ unsigned char *zOut = sqlite3_malloc( nIn*4 + 1 ); int c, sz, nOut; if( zOut==0 ) return 0; nOut = 0; while( nIn>0 ){ c = utf8Read(zIn, nIn, &sz); zIn += sz; nIn -= sz; if( c<=127 ){ zOut[nOut++] = c; }else{ int xTop, xBtm, x; |
︙ | ︙ | |||
2031 2032 2033 2034 2035 2036 2037 | } sqlite3_result_int(context, res); } /* End transliterate ****************************************************************************** ****************************************************************************** | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 | } sqlite3_result_int(context, res); } /* End transliterate ****************************************************************************** ****************************************************************************** ** Begin spellfix1 virtual table. */ /* Maximum length of a phonehash used for querying the shadow table */ #define SPELLFIX_MX_HASH 8 /* Maximum number of hash strings to examine per query */ #define SPELLFIX_MX_RUN 1 typedef struct spellfix1_vtab spellfix1_vtab; typedef struct spellfix1_cursor spellfix1_cursor; /* Fuzzy-search virtual table object */ struct spellfix1_vtab { sqlite3_vtab base; /* Base class - must be first */ |
︙ | ︙ | |||
2183 2184 2185 2186 2187 2188 2189 | struct spellfix1_cursor { sqlite3_vtab_cursor base; /* Base class - must be first */ spellfix1_vtab *pVTab; /* The table to which this cursor belongs */ char *zPattern; /* rhs of MATCH clause */ int nRow; /* Number of rows of content */ int nAlloc; /* Number of allocated rows */ int iRow; /* Current row of content */ | | > | 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 | struct spellfix1_cursor { sqlite3_vtab_cursor base; /* Base class - must be first */ spellfix1_vtab *pVTab; /* The table to which this cursor belongs */ char *zPattern; /* rhs of MATCH clause */ int nRow; /* Number of rows of content */ int nAlloc; /* Number of allocated rows */ int iRow; /* Current row of content */ int iLang; /* Value of the langid= constraint */ int iTop; /* Value of the top= constraint */ int iScope; /* Value of the scope= constraint */ int nSearch; /* Number of vocabulary items checked */ sqlite3_stmt *pFullScan; /* Shadow query for a full table scan */ struct spellfix1_row { /* For each row of content */ sqlite3_int64 iRowid; /* Rowid for this row */ char *zWord; /* Text for this row */ int iRank; /* Rank for this row */ int iDistance; /* Distance from pattern for this row */ int iScore; /* Score for sorting */ int iMatchlen; /* Value of matchlen column (or -1) */ |
︙ | ︙ | |||
2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 | char *zOut; int i, j; char c; while( isspace(zIn[0]) ) zIn++; zOut = sqlite3_mprintf("%s", zIn); if( zOut==0 ) return 0; i = (int)strlen(zOut); while( i>0 && isspace(zOut[i-1]) ){ i--; } zOut[i] = 0; c = zOut[0]; if( c=='\'' || c=='"' ){ | > > | | 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 | char *zOut; int i, j; char c; while( isspace(zIn[0]) ) zIn++; zOut = sqlite3_mprintf("%s", zIn); if( zOut==0 ) return 0; i = (int)strlen(zOut); #if 0 /* The parser will never leave spaces at the end */ while( i>0 && isspace(zOut[i-1]) ){ i--; } #endif zOut[i] = 0; c = zOut[0]; if( c=='\'' || c=='"' ){ for(i=1, j=0; ALWAYS(zOut[i]); i++){ zOut[j++] = zOut[i]; if( zOut[i]==c ){ if( zOut[i+1]==c ){ i++; }else{ zOut[j-1] = 0; break; |
︙ | ︙ | |||
2307 2308 2309 2310 2311 2312 2313 | const char *zModule = argv[0]; const char *zDbName = argv[1]; const char *zTableName = argv[2]; int nDbName; int rc = SQLITE_OK; int i; | < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | | | | | > > > | > | 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 | const char *zModule = argv[0]; const char *zDbName = argv[1]; const char *zTableName = argv[2]; int nDbName; int rc = SQLITE_OK; int i; nDbName = strlen(zDbName); pNew = sqlite3_malloc( sizeof(*pNew) + nDbName + 1); if( pNew==0 ){ rc = SQLITE_NOMEM; }else{ memset(pNew, 0, sizeof(*pNew)); pNew->zDbName = (char*)&pNew[1]; memcpy(pNew->zDbName, zDbName, nDbName+1); pNew->zTableName = sqlite3_mprintf("%s", zTableName); pNew->db = db; if( pNew->zTableName==0 ){ rc = SQLITE_NOMEM; }else{ rc = sqlite3_declare_vtab(db, "CREATE TABLE x(word,rank,distance,langid, " "score, matchlen, phonehash HIDDEN, " "top HIDDEN, scope HIDDEN, srchcnt HIDDEN, " "soundslike HIDDEN, command HIDDEN)" ); #define SPELLFIX_COL_WORD 0 #define SPELLFIX_COL_RANK 1 #define SPELLFIX_COL_DISTANCE 2 #define SPELLFIX_COL_LANGID 3 #define SPELLFIX_COL_SCORE 4 #define SPELLFIX_COL_MATCHLEN 5 #define SPELLFIX_COL_PHONEHASH 6 #define SPELLFIX_COL_TOP 7 #define SPELLFIX_COL_SCOPE 8 #define SPELLFIX_COL_SRCHCNT 9 #define SPELLFIX_COL_SOUNDSLIKE 10 #define SPELLFIX_COL_COMMAND 11 } if( rc==SQLITE_OK && isCreate ){ sqlite3_uint64 r; spellfix1DbExec(&rc, db, "CREATE TABLE IF NOT EXISTS \"%w\".\"%w_vocab\"(\n" " id INTEGER PRIMARY KEY,\n" " rank INT,\n" " langid INT,\n" " word TEXT,\n" " k1 TEXT,\n" " k2 TEXT\n" ");\n", zDbName, zTableName ); sqlite3_randomness(sizeof(r), &r); spellfix1DbExec(&rc, db, "CREATE INDEX IF NOT EXISTS \"%w\".\"%w_index_%llx\" " "ON \"%w_vocab\"(langid,k2);", zDbName, zModule, r, zTableName ); } for(i=3; rc==SQLITE_OK && i<argc; i++){ if( memcmp(argv[i],"edit_cost_table=",16)==0 && pNew->zCostTable==0 ){ pNew->zCostTable = spellfix1Dequote(&argv[i][16]); if( pNew->zCostTable==0 ) rc = SQLITE_NOMEM; continue; } *pzErr = sqlite3_mprintf("bad argument to spellfix1(): \"%s\"", argv[i]); rc = SQLITE_ERROR; } } if( rc && pNew ){ *ppVTab = 0; spellfix1Uninit(0, &pNew->base); }else{ *ppVTab = (sqlite3_vtab *)pNew; } return rc; } /* ** The xConnect and xCreate methods */ static int spellfix1Connect( |
︙ | ︙ | |||
2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 | int i; for(i=0; i<pCur->nRow; i++){ sqlite3_free(pCur->a[i].zWord); } pCur->nRow = 0; pCur->iRow = 0; pCur->nSearch = 0; } /* ** Resize the cursor to hold up to N rows of content */ static void spellfix1ResizeCursor(spellfix1_cursor *pCur, int N){ struct spellfix1_row *aNew; | > > > > | 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 | int i; for(i=0; i<pCur->nRow; i++){ sqlite3_free(pCur->a[i].zWord); } pCur->nRow = 0; pCur->iRow = 0; pCur->nSearch = 0; if( pCur->pFullScan ){ sqlite3_finalize(pCur->pFullScan); pCur->pFullScan = 0; } } /* ** Resize the cursor to hold up to N rows of content */ static void spellfix1ResizeCursor(spellfix1_cursor *pCur, int N){ struct spellfix1_row *aNew; |
︙ | ︙ | |||
2531 2532 2533 2534 2535 2536 2537 | iDistTerm = i; } } if( iPlan&1 ){ int idx = 2; pIdxInfo->idxNum = iPlan; if( pIdxInfo->nOrderBy==1 | | | 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 | iDistTerm = i; } } if( iPlan&1 ){ int idx = 2; pIdxInfo->idxNum = iPlan; if( pIdxInfo->nOrderBy==1 && pIdxInfo->aOrderBy[0].iColumn==SPELLFIX_COL_SCORE && pIdxInfo->aOrderBy[0].desc==0 ){ pIdxInfo->orderByConsumed = 1; /* Default order by iScore */ } if( iPlan&2 ){ pIdxInfo->aConstraintUsage[iLangTerm].argvIndex = idx++; pIdxInfo->aConstraintUsage[iLangTerm].omit = 1; |
︙ | ︙ | |||
2636 2637 2638 2639 2640 2641 2642 2643 2644 | int iScope = p->iScope; spellfix1_cursor *pCur = p->pCur; sqlite3_stmt *pStmt = p->pStmt; char zHash1[SPELLFIX_MX_HASH]; char zHash2[SPELLFIX_MX_HASH]; char *zClass; int nClass; if( pCur->a==0 || p->rc ) return; /* Prior memory allocation failure */ | > < | 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 | int iScope = p->iScope; spellfix1_cursor *pCur = p->pCur; sqlite3_stmt *pStmt = p->pStmt; char zHash1[SPELLFIX_MX_HASH]; char zHash2[SPELLFIX_MX_HASH]; char *zClass; int nClass; int rc; if( pCur->a==0 || p->rc ) return; /* Prior memory allocation failure */ zClass = (char*)phoneticHash((unsigned char*)zQuery, nQuery); if( zClass==0 ){ p->rc = SQLITE_NOMEM; return; } nClass = strlen(zClass); if( nClass>SPELLFIX_MX_HASH-2 ){ |
︙ | ︙ | |||
2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 | } memcpy(zHash1, zClass, iScope); sqlite3_free(zClass); zHash1[iScope] = 0; memcpy(zHash2, zHash1, iScope); zHash2[iScope] = 'Z'; zHash2[iScope+1] = 0; for(i=0; i<p->nRun; i++){ if( strcmp(p->azPrior[i], zHash1)==0 ) return; } memcpy(p->azPrior[p->nRun++], zHash1, iScope+1); | > > > | | > > > > > > | > > > > > > > > | > | | 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 | } memcpy(zHash1, zClass, iScope); sqlite3_free(zClass); zHash1[iScope] = 0; memcpy(zHash2, zHash1, iScope); zHash2[iScope] = 'Z'; zHash2[iScope+1] = 0; #if SPELLFIX_MX_RUN>1 for(i=0; i<p->nRun; i++){ if( strcmp(p->azPrior[i], zHash1)==0 ) return; } #endif assert( p->nRun<SPELLFIX_MX_RUN ); memcpy(p->azPrior[p->nRun++], zHash1, iScope+1); if( sqlite3_bind_text(pStmt, 1, zHash1, -1, SQLITE_STATIC)==SQLITE_NOMEM || sqlite3_bind_text(pStmt, 2, zHash2, -1, SQLITE_STATIC)==SQLITE_NOMEM ){ p->rc = SQLITE_NOMEM; return; } #if SPELLFIX_MX_RUN>1 for(i=0; i<pCur->nRow; i++){ if( pCur->a[i].iScore>iWorst ){ iWorst = pCur->a[i].iScore; idxWorst = i; } } #endif while( sqlite3_step(pStmt)==SQLITE_ROW ){ int iMatchlen = -1; iRank = sqlite3_column_int(pStmt, 2); if( p->pMatchStr3 ){ int nWord = sqlite3_column_bytes(pStmt, 1); zWord = (const char*)sqlite3_column_text(pStmt, 1); iDist = editDist3Core(p->pMatchStr3, zWord, nWord, p->pLang, &iMatchlen); }else{ zK1 = (const char*)sqlite3_column_text(pStmt, 3); if( zK1==0 ) continue; iDist = editdist1(p->zPattern, zK1, 0); } if( iDist<0 ){ p->rc = SQLITE_NOMEM; break; } pCur->nSearch++; iScore = spellfix1Score(iDist,iRank); if( p->iMaxDist>=0 ){ if( iDist>p->iMaxDist ) continue; if( pCur->nRow>=pCur->nAlloc-1 ){ spellfix1ResizeCursor(pCur, pCur->nAlloc*2 + 10); if( pCur->a==0 ) break; } idx = pCur->nRow; }else if( pCur->nRow<pCur->nAlloc ){ idx = pCur->nRow; }else if( iScore<iWorst ){ idx = idxWorst; sqlite3_free(pCur->a[idx].zWord); }else{ continue; } pCur->a[idx].zWord = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 1)); if( pCur->a[idx].zWord==0 ){ p->rc = SQLITE_NOMEM; break; } pCur->a[idx].iRowid = sqlite3_column_int64(pStmt, 0); pCur->a[idx].iRank = iRank; pCur->a[idx].iDistance = iDist; pCur->a[idx].iScore = iScore; pCur->a[idx].iMatchlen = iMatchlen; memcpy(pCur->a[idx].zHash, zHash1, iScope+1); if( pCur->nRow<pCur->nAlloc ) pCur->nRow++; if( pCur->nRow==pCur->nAlloc ){ iWorst = pCur->a[0].iScore; idxWorst = 0; for(i=1; i<pCur->nRow; i++){ iScore = pCur->a[i].iScore; if( iWorst<iScore ){ iWorst = iScore; idxWorst = i; } } } } rc = sqlite3_reset(pStmt); if( rc ) p->rc = rc; } /* ** This version of the xFilter method work if the MATCH term is present ** and we are doing a scan. */ static int spellfix1FilterForMatch( spellfix1_cursor *pCur, int idxNum, int argc, sqlite3_value **argv ){ const unsigned char *zMatchThis; /* RHS of the MATCH operator */ EditDist3FromString *pMatchStr3 = 0; /* zMatchThis as an editdist string */ char *zPattern; /* Transliteration of zMatchThis */ int nPattern; /* Length of zPattern */ int iLimit = 20; /* Max number of rows of output */ int iScope = 3; /* Use this many characters of zClass */ int iLang = 0; /* Language code */ char *zSql; /* SQL of shadow table query */ sqlite3_stmt *pStmt = 0; /* Shadow table query */ int rc; /* Result code */ int idx = 1; /* Next available filter parameter */ spellfix1_vtab *p = pCur->pVTab; /* The virtual table that owns pCur */ MatchQuery x; /* For passing info to RunQuery() */ /* Load the cost table if we have not already done so */ if( p->zCostTable!=0 && p->pConfig3==0 ){ |
︙ | ︙ | |||
2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 | spellfix1ResetCursor(pCur); spellfix1ResizeCursor(pCur, iLimit); zMatchThis = sqlite3_value_text(argv[0]); if( zMatchThis==0 ) return SQLITE_OK; if( p->pConfig3 ){ x.pLang = editDist3FindLang(p->pConfig3, iLang); pMatchStr3 = editDist3FromStringNew(x.pLang, (const char*)zMatchThis, -1); }else{ x.pLang = 0; } zPattern = (char*)transliterate(zMatchThis, sqlite3_value_bytes(argv[0])); sqlite3_free(pCur->zPattern); pCur->zPattern = zPattern; | > > > > | > > > > > > > > < < < < < < < < < < < < < < < < < < < < > > > > | > > > > > > > > > > > > > > > | > | | 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 | spellfix1ResetCursor(pCur); spellfix1ResizeCursor(pCur, iLimit); zMatchThis = sqlite3_value_text(argv[0]); if( zMatchThis==0 ) return SQLITE_OK; if( p->pConfig3 ){ x.pLang = editDist3FindLang(p->pConfig3, iLang); pMatchStr3 = editDist3FromStringNew(x.pLang, (const char*)zMatchThis, -1); if( pMatchStr3==0 ){ x.rc = SQLITE_NOMEM; goto filter_exit; } }else{ x.pLang = 0; } zPattern = (char*)transliterate(zMatchThis, sqlite3_value_bytes(argv[0])); sqlite3_free(pCur->zPattern); pCur->zPattern = zPattern; if( zPattern==0 ){ x.rc = SQLITE_NOMEM; goto filter_exit; } nPattern = strlen(zPattern); if( zPattern[nPattern-1]=='*' ) nPattern--; zSql = sqlite3_mprintf( "SELECT id, word, rank, k1" " FROM \"%w\".\"%w_vocab\"" " WHERE langid=%d AND k2>=?1 AND k2<?2", p->zDbName, p->zTableName, iLang ); if( zSql==0 ){ x.rc = SQLITE_NOMEM; pStmt = 0; goto filter_exit; } rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); sqlite3_free(zSql); pCur->iLang = iLang; x.pCur = pCur; x.pStmt = pStmt; x.zPattern = zPattern; x.nPattern = nPattern; x.pMatchStr3 = pMatchStr3; x.iLang = iLang; x.rc = rc; x.pConfig3 = p->pConfig3; if( x.rc==SQLITE_OK ){ spellfix1RunQuery(&x, zPattern, nPattern); } if( pCur->a ){ qsort(pCur->a, pCur->nRow, sizeof(pCur->a[0]), spellfix1RowCompare); pCur->iTop = iLimit; pCur->iScope = iScope; }else{ x.rc = SQLITE_NOMEM; } filter_exit: sqlite3_finalize(pStmt); editDist3FromStringDelete(pMatchStr3); return x.rc; } /* ** This version of xFilter handles a full-table scan case */ static int spellfix1FilterForFullScan( spellfix1_cursor *pCur, int idxNum, int argc, sqlite3_value **argv ){ int rc; char *zSql; spellfix1_vtab *pVTab = pCur->pVTab; spellfix1ResetCursor(pCur); zSql = sqlite3_mprintf( "SELECT word, rank, NULL, langid, id FROM \"%w\".\"%w_vocab\"", pVTab->zDbName, pVTab->zTableName); if( zSql==0 ) return SQLITE_NOMEM; rc = sqlite3_prepare_v2(pVTab->db, zSql, -1, &pCur->pFullScan, 0); sqlite3_free(zSql); pCur->nRow = pCur->iRow = 0; if( rc==SQLITE_OK ){ rc = sqlite3_step(pCur->pFullScan); if( rc==SQLITE_ROW ){ pCur->iRow = -1; rc = SQLITE_OK; } if( rc==SQLITE_DONE ){ rc = SQLITE_OK; } }else{ pCur->iRow = 0; } return rc; } /* ** Called to "rewind" a cursor back to the beginning so that ** it starts its output over again. Always called at least once ** prior to any spellfix1Column, spellfix1Rowid, or spellfix1Eof call. |
︙ | ︙ | |||
2887 2888 2889 2890 2891 2892 2893 | /* ** Advance a cursor to its next row of output */ static int spellfix1Next(sqlite3_vtab_cursor *cur){ spellfix1_cursor *pCur = (spellfix1_cursor *)cur; | | > > > > > > > | > > > > > > > > > > > > | 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 | /* ** Advance a cursor to its next row of output */ static int spellfix1Next(sqlite3_vtab_cursor *cur){ spellfix1_cursor *pCur = (spellfix1_cursor *)cur; if( pCur->iRow < pCur->nRow ){ if( pCur->pFullScan ){ int rc = sqlite3_step(pCur->pFullScan); if( rc!=SQLITE_ROW ) pCur->iRow = pCur->nRow; }else{ pCur->iRow++; } } return SQLITE_OK; } /* ** Return TRUE if we are at the end-of-file */ static int spellfix1Eof(sqlite3_vtab_cursor *cur){ spellfix1_cursor *pCur = (spellfix1_cursor *)cur; return pCur->iRow>=pCur->nRow; } /* ** Return columns from the current row. */ static int spellfix1Column( sqlite3_vtab_cursor *cur, sqlite3_context *ctx, int i ){ spellfix1_cursor *pCur = (spellfix1_cursor*)cur; if( pCur->pFullScan ){ if( i<=SPELLFIX_COL_LANGID ){ sqlite3_result_value(ctx, sqlite3_column_value(pCur->pFullScan, i)); }else{ sqlite3_result_null(ctx); } return SQLITE_OK; } switch( i ){ case SPELLFIX_COL_WORD: { sqlite3_result_text(ctx, pCur->a[pCur->iRow].zWord, -1, SQLITE_STATIC); break; } case SPELLFIX_COL_RANK: { sqlite3_result_int(ctx, pCur->a[pCur->iRow].iRank); |
︙ | ︙ | |||
2937 2938 2939 2940 2941 2942 2943 | int nWord = strlen(zWord); if( nPattern>0 && pCur->zPattern[nPattern-1]=='*' ){ char *zTranslit; int res; zTranslit = (char *)transliterate((unsigned char *)zWord, nWord); if( !zTranslit ) return SQLITE_NOMEM; | | | 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 | int nWord = strlen(zWord); if( nPattern>0 && pCur->zPattern[nPattern-1]=='*' ){ char *zTranslit; int res; zTranslit = (char *)transliterate((unsigned char *)zWord, nWord); if( !zTranslit ) return SQLITE_NOMEM; res = editdist1(pCur->zPattern, zTranslit, &iMatchlen); sqlite3_free(zTranslit); if( res<0 ) return SQLITE_NOMEM; iMatchlen = translen_to_charlen(zWord, nWord, iMatchlen); }else{ iMatchlen = utf8Charlen(zWord, nWord); } } |
︙ | ︙ | |||
2978 2979 2980 2981 2982 2983 2984 | } /* ** The rowid. */ static int spellfix1Rowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ spellfix1_cursor *pCur = (spellfix1_cursor*)cur; | > > > | > | 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 | } /* ** The rowid. */ static int spellfix1Rowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ spellfix1_cursor *pCur = (spellfix1_cursor*)cur; if( pCur->pFullScan ){ *pRowid = sqlite3_column_int64(pCur->pFullScan, 4); }else{ *pRowid = pCur->a[pCur->iRow].iRowid; } return SQLITE_OK; } /* ** The xUpdate() method. */ static int spellfix1Update( |
︙ | ︙ | |||
3062 3063 3064 3065 3066 3067 3068 | iRank, iLang, zWord, zK1, zK2 ); *pRowid = sqlite3_last_insert_rowid(db); }else{ rowid = sqlite3_value_int64(argv[0]); newRowid = *pRowid = sqlite3_value_int64(argv[1]); spellfix1DbExec(&rc, db, | | | | 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 | iRank, iLang, zWord, zK1, zK2 ); *pRowid = sqlite3_last_insert_rowid(db); }else{ rowid = sqlite3_value_int64(argv[0]); newRowid = *pRowid = sqlite3_value_int64(argv[1]); spellfix1DbExec(&rc, db, "UPDATE \"%w\".\"%w_vocab\" SET id=%lld, rank=%d, langid=%d," " word=%Q, k1=%Q, k2=%Q WHERE id=%lld", p->zDbName, p->zTableName, newRowid, iRank, iLang, zWord, zK1, zK2, rowid ); } sqlite3_free(zK1); sqlite3_free(zK2); } |
︙ | ︙ | |||
3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 | spellfix1DbExec(&rc, db, "ALTER TABLE \"%w\".\"%w_vocab\" RENAME TO \"%w_vocab\"", p->zDbName, p->zTableName, zNewName ); if( rc==SQLITE_OK ){ sqlite3_free(p->zTableName); p->zTableName = zNewName; } return rc; } /* ** A virtual table module that provides fuzzy search. | > > | 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 | spellfix1DbExec(&rc, db, "ALTER TABLE \"%w\".\"%w_vocab\" RENAME TO \"%w_vocab\"", p->zDbName, p->zTableName, zNewName ); if( rc==SQLITE_OK ){ sqlite3_free(p->zTableName); p->zTableName = zNewName; }else{ sqlite3_free(zNewName); } return rc; } /* ** A virtual table module that provides fuzzy search. |
︙ | ︙ | |||
3133 3134 3135 3136 3137 3138 3139 | static int spellfix1Register(sqlite3 *db){ int nErr = 0; int i; nErr += sqlite3_create_function(db, "spellfix1_translit", 1, SQLITE_UTF8, 0, transliterateSqlFunc, 0, 0); nErr += sqlite3_create_function(db, "spellfix1_editdist", 2, SQLITE_UTF8, 0, editdistSqlFunc, 0, 0); | < < < < < < | 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 | static int spellfix1Register(sqlite3 *db){ int nErr = 0; int i; nErr += sqlite3_create_function(db, "spellfix1_translit", 1, SQLITE_UTF8, 0, transliterateSqlFunc, 0, 0); nErr += sqlite3_create_function(db, "spellfix1_editdist", 2, SQLITE_UTF8, 0, editdistSqlFunc, 0, 0); nErr += sqlite3_create_function(db, "spellfix1_phonehash", 1, SQLITE_UTF8, 0, phoneticHashSqlFunc, 0, 0); nErr += sqlite3_create_function(db, "spellfix1_scriptcode", 1, SQLITE_UTF8, 0, scriptCodeSqlFunc, 0, 0); nErr += sqlite3_create_module(db, "spellfix1", &spellfix1Module, 0); nErr += editDist3Install(db); /* Verify sanity of the translit[] table */ for(i=0; i<sizeof(translit)/sizeof(translit[0])-1; i++){ assert( translit[i].cFrom<translit[i+1].cFrom ); } |
︙ | ︙ |
Changes to test/spellfix.test.
︙ | ︙ | |||
133 134 135 136 137 138 139 | foreach w $vocab { execsql { INSERT INTO t3(word) VALUES($w) } } } {} breakpoint foreach {tn word res} { | | | | > | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | foreach w $vocab { execsql { INSERT INTO t3(word) VALUES($w) } } } {} breakpoint foreach {tn word res} { 1 kos* {kosher 3 kiosk 4 kudo 2 kiss 3 kissed 3} 2 kellj* {killjoy 5 kill 4 killed 4 killer 4 killers 4} 3 kellj {kill 4 kills 5 killjoy 7 keel 4 killed 6} } { do_execsql_test 1.2.$tn { SELECT word, matchlen FROM t3 WHERE word MATCH $word ORDER BY score, word LIMIT 5 } $res } finish_test |