| | @@ -921,29 +921,38 @@ |
| 921 | 921 | unsigned int srchFlags /* What to search over */ |
| 922 | 922 | ){ |
| 923 | 923 | Blob sql; |
| 924 | 924 | char *zPat = mprintf("%s",zPattern); |
| 925 | 925 | int i; |
| 926 | + static const char *zSnippetCall; |
| 926 | 927 | if( srchFlags==0 ) return; |
| 927 | 928 | sqlite3_create_function(g.db, "rank", 1, SQLITE_UTF8|SQLITE_INNOCUOUS, 0, |
| 928 | 929 | search_rank_sqlfunc, 0, 0); |
| 929 | 930 | for(i=0; zPat[i]; i++){ |
| 930 | 931 | if( zPat[i]=='-' || zPat[i]=='"' ) zPat[i] = ' '; |
| 931 | 932 | } |
| 932 | 933 | blob_init(&sql, 0, 0); |
| 934 | + if( search_index_type(0)==4 ){ |
| 935 | + /* If this repo is still using the legacy FTS4 search index, then |
| 936 | + ** the snippet() function is slightly different */ |
| 937 | + zSnippetCall = "snippet(ftsidx,'<mark>','</mark>',' ... ',-1,35)"; |
| 938 | + }else{ |
| 939 | + /* This is the common case - Using newer FTS5 search index */ |
| 940 | + zSnippetCall = "snippet(ftsidx,-1,'<mark>','</mark>',' ... ',35)"; |
| 941 | + } |
| 933 | 942 | blob_appendf(&sql, |
| 934 | 943 | "INSERT INTO x(label,url,score,id,date,snip) " |
| 935 | 944 | " SELECT ftsdocs.label," |
| 936 | 945 | " ftsdocs.url," |
| 937 | 946 | " rank(matchinfo(ftsidx,'pcsx'))," |
| 938 | 947 | " ftsdocs.type || ftsdocs.rid," |
| 939 | 948 | " datetime(ftsdocs.mtime)," |
| 940 | | - " snippet(ftsidx,'<mark>','</mark>',' ... ',-1,35)" |
| 949 | + " %s" |
| 941 | 950 | " FROM ftsidx CROSS JOIN ftsdocs" |
| 942 | 951 | " WHERE ftsidx MATCH %Q" |
| 943 | | - " AND ftsdocs.rowid=ftsidx.docid", |
| 944 | | - zPat |
| 952 | + " AND ftsdocs.rowid=ftsidx.rowid", |
| 953 | + zSnippetCall /*safe-for-%s*/, zPat |
| 945 | 954 | ); |
| 946 | 955 | fossil_free(zPat); |
| 947 | 956 | if( srchFlags!=SRCH_ALL ){ |
| 948 | 957 | const char *zSep = " AND ("; |
| 949 | 958 | static const struct { unsigned m; char c; } aMask[] = { |
| | @@ -1508,11 +1517,11 @@ |
| 1508 | 1517 | /* The schema for the full-text index |
| 1509 | 1518 | */ |
| 1510 | 1519 | static const char zFtsSchema[] = |
| 1511 | 1520 | @ -- One entry for each possible search result |
| 1512 | 1521 | @ CREATE TABLE IF NOT EXISTS repository.ftsdocs( |
| 1513 | | -@ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid |
| 1522 | +@ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.rowid |
| 1514 | 1523 | @ type CHAR(1), -- Type of document |
| 1515 | 1524 | @ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document |
| 1516 | 1525 | @ name TEXT, -- Additional document description |
| 1517 | 1526 | @ idxed BOOLEAN, -- True if currently in the index |
| 1518 | 1527 | @ label TEXT, -- Label to print on search results |
| | @@ -1526,11 +1535,11 @@ |
| 1526 | 1535 | @ CREATE VIEW IF NOT EXISTS repository.ftscontent AS |
| 1527 | 1536 | @ SELECT rowid, type, rid, name, idxed, label, url, mtime, |
| 1528 | 1537 | @ title(type,rid,name) AS 'title', body(type,rid,name) AS 'body' |
| 1529 | 1538 | @ FROM ftsdocs; |
| 1530 | 1539 | @ CREATE VIRTUAL TABLE IF NOT EXISTS repository.ftsidx |
| 1531 | | -@ USING fts4(content="ftscontent", title, body%s); |
| 1540 | +@ USING fts5(content="ftscontent", title, body%s); |
| 1532 | 1541 | ; |
| 1533 | 1542 | static const char zFtsDrop[] = |
| 1534 | 1543 | @ DROP TABLE IF EXISTS repository.ftsidx; |
| 1535 | 1544 | @ DROP VIEW IF EXISTS repository.ftscontent; |
| 1536 | 1545 | @ DROP TABLE IF EXISTS repository.ftsdocs; |
| | @@ -1551,18 +1560,41 @@ |
| 1551 | 1560 | db_multi_exec(zFtsDrop/*works-like:""*/); |
| 1552 | 1561 | searchIdxExists = 0; |
| 1553 | 1562 | } |
| 1554 | 1563 | |
| 1555 | 1564 | /* |
| 1556 | | -** Return true if the full-text search index exists |
| 1565 | +** Return true if the full-text search index exists. See also the |
| 1566 | +** search_index_type() function. |
| 1557 | 1567 | */ |
| 1558 | 1568 | int search_index_exists(void){ |
| 1559 | 1569 | if( searchIdxExists<0 ){ |
| 1560 | 1570 | searchIdxExists = db_table_exists("repository","ftsdocs"); |
| 1561 | 1571 | } |
| 1562 | 1572 | return searchIdxExists; |
| 1563 | 1573 | } |
| 1574 | + |
| 1575 | +/* |
| 1576 | +** Determine which full-text search index is currently being used to |
| 1577 | +** add searching. Return values: |
| 1578 | +** |
| 1579 | +** 0 No search index is available |
| 1580 | +** 4 FTS3/4 |
| 1581 | +** 5 FTS5 |
| 1582 | +** |
| 1583 | +** Results are cached. Make the argument 1 to reset the cache. See |
| 1584 | +** also the search_index_exists() routine. |
| 1585 | +*/ |
| 1586 | +int search_index_type(int bReset){ |
| 1587 | + static int idxType = -1; |
| 1588 | + if( idxType<0 || bReset ){ |
| 1589 | + idxType = db_int(0, |
| 1590 | + "SELECT CASE WHEN sql GLOB '*fts4*' THEN 4 ELSE 5 END" |
| 1591 | + " FROM repository.sqlite_schema WHERE name='ftsidx'" |
| 1592 | + ); |
| 1593 | + } |
| 1594 | + return idxType; |
| 1595 | +} |
| 1564 | 1596 | |
| 1565 | 1597 | /* |
| 1566 | 1598 | ** Fill the FTSDOCS table with unindexed entries for everything |
| 1567 | 1599 | ** in the repository. This uses INSERT OR IGNORE so entries already |
| 1568 | 1600 | ** in FTSDOCS are unchanged. |
| | @@ -1606,11 +1638,11 @@ |
| 1606 | 1638 | char zType[2]; |
| 1607 | 1639 | zType[0] = cType; |
| 1608 | 1640 | zType[1] = 0; |
| 1609 | 1641 | search_sql_setup(g.db); |
| 1610 | 1642 | db_multi_exec( |
| 1611 | | - "DELETE FROM ftsidx WHERE docid IN" |
| 1643 | + "DELETE FROM ftsidx WHERE rowid IN" |
| 1612 | 1644 | " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", |
| 1613 | 1645 | zType, rid |
| 1614 | 1646 | ); |
| 1615 | 1647 | db_multi_exec( |
| 1616 | 1648 | "REPLACE INTO ftsdocs(type,rid,name,idxed)" |
| | @@ -1617,11 +1649,11 @@ |
| 1617 | 1649 | " VALUES(%Q,%d,%Q,0)", |
| 1618 | 1650 | zType, rid, zName |
| 1619 | 1651 | ); |
| 1620 | 1652 | if( cType=='w' || cType=='e' ){ |
| 1621 | 1653 | db_multi_exec( |
| 1622 | | - "DELETE FROM ftsidx WHERE docid IN" |
| 1654 | + "DELETE FROM ftsidx WHERE rowid IN" |
| 1623 | 1655 | " (SELECT rowid FROM ftsdocs WHERE type='%c' AND name=%Q AND idxed)", |
| 1624 | 1656 | cType, zName |
| 1625 | 1657 | ); |
| 1626 | 1658 | db_multi_exec( |
| 1627 | 1659 | "DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d", |
| | @@ -1657,11 +1689,11 @@ |
| 1657 | 1689 | " WHERE foci.checkinID=%d AND blob.uuid=foci.uuid" |
| 1658 | 1690 | " AND %z", |
| 1659 | 1691 | ckid, glob_expr("foci.filename", db_get("doc-glob","")) |
| 1660 | 1692 | ); |
| 1661 | 1693 | db_multi_exec( |
| 1662 | | - "DELETE FROM ftsidx WHERE docid IN" |
| 1694 | + "DELETE FROM ftsidx WHERE rowid IN" |
| 1663 | 1695 | " (SELECT rowid FROM ftsdocs WHERE type='d'" |
| 1664 | 1696 | " AND rid NOT IN (SELECT rid FROM current_docs))" |
| 1665 | 1697 | ); |
| 1666 | 1698 | db_multi_exec( |
| 1667 | 1699 | "DELETE FROM ftsdocs WHERE type='d'" |
| | @@ -1676,11 +1708,11 @@ |
| 1676 | 1708 | " %.17g" |
| 1677 | 1709 | " FROM current_docs", |
| 1678 | 1710 | zDocBr, rTime |
| 1679 | 1711 | ); |
| 1680 | 1712 | db_multi_exec( |
| 1681 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1713 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1682 | 1714 | " SELECT rowid, label, bx FROM ftsdocs WHERE type='d' AND NOT idxed" |
| 1683 | 1715 | ); |
| 1684 | 1716 | db_multi_exec( |
| 1685 | 1717 | "UPDATE ftsdocs SET" |
| 1686 | 1718 | " idxed=1," |
| | @@ -1693,11 +1725,11 @@ |
| 1693 | 1725 | /* |
| 1694 | 1726 | ** Deal with all of the unindexed 'c' terms in FTSDOCS |
| 1695 | 1727 | */ |
| 1696 | 1728 | static void search_update_checkin_index(void){ |
| 1697 | 1729 | db_multi_exec( |
| 1698 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1730 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1699 | 1731 | " SELECT rowid, '', body('c',rid,NULL) FROM ftsdocs" |
| 1700 | 1732 | " WHERE type='c' AND NOT idxed;" |
| 1701 | 1733 | ); |
| 1702 | 1734 | db_multi_exec( |
| 1703 | 1735 | "UPDATE ftsdocs SET idxed=1, name=NULL," |
| | @@ -1716,11 +1748,11 @@ |
| 1716 | 1748 | /* |
| 1717 | 1749 | ** Deal with all of the unindexed 't' terms in FTSDOCS |
| 1718 | 1750 | */ |
| 1719 | 1751 | static void search_update_ticket_index(void){ |
| 1720 | 1752 | db_multi_exec( |
| 1721 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1753 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1722 | 1754 | " SELECT rowid, title('t',rid,NULL), body('t',rid,NULL) FROM ftsdocs" |
| 1723 | 1755 | " WHERE type='t' AND NOT idxed;" |
| 1724 | 1756 | ); |
| 1725 | 1757 | if( db_changes()==0 ) return; |
| 1726 | 1758 | db_multi_exec( |
| | @@ -1739,11 +1771,11 @@ |
| 1739 | 1771 | /* |
| 1740 | 1772 | ** Deal with all of the unindexed 'w' terms in FTSDOCS |
| 1741 | 1773 | */ |
| 1742 | 1774 | static void search_update_wiki_index(void){ |
| 1743 | 1775 | db_multi_exec( |
| 1744 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1776 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1745 | 1777 | " SELECT rowid, title('w',rid,NULL),body('w',rid,NULL) FROM ftsdocs" |
| 1746 | 1778 | " WHERE type='w' AND NOT idxed;" |
| 1747 | 1779 | ); |
| 1748 | 1780 | if( db_changes()==0 ) return; |
| 1749 | 1781 | db_multi_exec( |
| | @@ -1761,11 +1793,11 @@ |
| 1761 | 1793 | /* |
| 1762 | 1794 | ** Deal with all of the unindexed 'f' terms in FTSDOCS |
| 1763 | 1795 | */ |
| 1764 | 1796 | static void search_update_forum_index(void){ |
| 1765 | 1797 | db_multi_exec( |
| 1766 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1798 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1767 | 1799 | " SELECT rowid, title('f',rid,NULL),body('f',rid,NULL) FROM ftsdocs" |
| 1768 | 1800 | " WHERE type='f' AND NOT idxed;" |
| 1769 | 1801 | ); |
| 1770 | 1802 | if( db_changes()==0 ) return; |
| 1771 | 1803 | db_multi_exec( |
| | @@ -1784,11 +1816,11 @@ |
| 1784 | 1816 | /* |
| 1785 | 1817 | ** Deal with all of the unindexed 'e' terms in FTSDOCS |
| 1786 | 1818 | */ |
| 1787 | 1819 | static void search_update_technote_index(void){ |
| 1788 | 1820 | db_multi_exec( |
| 1789 | | - "INSERT INTO ftsidx(docid,title,body)" |
| 1821 | + "INSERT INTO ftsidx(rowid,title,body)" |
| 1790 | 1822 | " SELECT rowid, title('e',rid,NULL),body('e',rid,NULL) FROM ftsdocs" |
| 1791 | 1823 | " WHERE type='e' AND NOT idxed;" |
| 1792 | 1824 | ); |
| 1793 | 1825 | if( db_changes()==0 ) return; |
| 1794 | 1826 | db_multi_exec( |
| | @@ -1869,11 +1901,11 @@ |
| 1869 | 1901 | ** |
| 1870 | 1902 | ** The current search settings are displayed after any changes are applied. |
| 1871 | 1903 | ** Run this command with no arguments to simply see the settings. |
| 1872 | 1904 | */ |
| 1873 | 1905 | void fts_config_cmd(void){ |
| 1874 | | - static const struct { |
| 1906 | + static const struct { |
| 1875 | 1907 | int iCmd; |
| 1876 | 1908 | const char *z; |
| 1877 | 1909 | } aCmd[] = { |
| 1878 | 1910 | { 1, "reindex" }, |
| 1879 | 1911 | { 2, "index" }, |
| | @@ -1957,11 +1989,11 @@ |
| 1957 | 1989 | db_get_boolean(aSetng[i].zSetting,0) ? "on" : "off"); |
| 1958 | 1990 | } |
| 1959 | 1991 | fossil_print("%-17s %s\n", "Porter stemmer:", |
| 1960 | 1992 | db_get_boolean("search-stemmer",0) ? "on" : "off"); |
| 1961 | 1993 | if( search_index_exists() ){ |
| 1962 | | - fossil_print("%-17s enabled\n", "full-text index:"); |
| 1994 | + fossil_print("%-17s FTS%d\n", "full-text index:", search_index_type(1)); |
| 1963 | 1995 | fossil_print("%-17s %d\n", "documents:", |
| 1964 | 1996 | db_int(0, "SELECT count(*) FROM ftsdocs")); |
| 1965 | 1997 | }else{ |
| 1966 | 1998 | fossil_print("%-17s disabled\n", "full-text index:"); |
| 1967 | 1999 | } |
| | @@ -2002,24 +2034,24 @@ |
| 2002 | 2034 | const char *zUrl = db_column_text(&q,4); |
| 2003 | 2035 | const char *zDocId = db_column_text(&q,0); |
| 2004 | 2036 | char *zName; |
| 2005 | 2037 | char *z; |
| 2006 | 2038 | @ <table border=0> |
| 2007 | | - @ <tr><td align='right'>docid:<td> <td>%d(id) |
| 2039 | + @ <tr><td align='right'>rowid:<td> <td>%d(id) |
| 2008 | 2040 | @ <tr><td align='right'>id:<td><td>%s(zDocId) |
| 2009 | 2041 | @ <tr><td align='right'>name:<td><td>%h(db_column_text(&q,1)) |
| 2010 | 2042 | @ <tr><td align='right'>idxed:<td><td>%d(db_column_int(&q,2)) |
| 2011 | 2043 | @ <tr><td align='right'>label:<td><td>%h(db_column_text(&q,3)) |
| 2012 | 2044 | @ <tr><td align='right'>url:<td><td> |
| 2013 | 2045 | @ <a href='%R%s(zUrl)'>%h(zUrl)</a> |
| 2014 | 2046 | @ <tr><td align='right'>mtime:<td><td>%s(db_column_text(&q,5)) |
| 2015 | | - z = db_text(0, "SELECT title FROM ftsidx WHERE docid=%d",id); |
| 2047 | + z = db_text(0, "SELECT title FROM ftsidx WHERE rowid=%d",id); |
| 2016 | 2048 | if( z && z[0] ){ |
| 2017 | 2049 | @ <tr><td align="right">title:<td><td>%h(z) |
| 2018 | 2050 | fossil_free(z); |
| 2019 | 2051 | } |
| 2020 | | - z = db_text(0, "SELECT body FROM ftsidx WHERE docid=%d",id); |
| 2052 | + z = db_text(0, "SELECT body FROM ftsidx WHERE rowid=%d",id); |
| 2021 | 2053 | if( z && z[0] ){ |
| 2022 | 2054 | @ <tr><td align="right" valign="top">body:<td><td>%h(z) |
| 2023 | 2055 | fossil_free(z); |
| 2024 | 2056 | } |
| 2025 | 2057 | @ </table> |
| | @@ -2103,5 +2135,382 @@ |
| 2103 | 2135 | @ <th align="right">%d(cnt3) |
| 2104 | 2136 | @ </tfooter> |
| 2105 | 2137 | @ </table> |
| 2106 | 2138 | style_finish_page(); |
| 2107 | 2139 | } |
| 2140 | + |
| 2141 | + |
| 2142 | +/* |
| 2143 | +** The Fts5MatchinfoCtx bits were all taken verbatim from: |
| 2144 | +** |
| 2145 | +** https://sqlite.org/src/finfo?name=ext/fts5/fts5_test_mi.c |
| 2146 | +*/ |
| 2147 | + |
| 2148 | +typedef struct Fts5MatchinfoCtx Fts5MatchinfoCtx; |
| 2149 | + |
| 2150 | +#ifndef SQLITE_AMALGAMATION |
| 2151 | +typedef unsigned int u32; |
| 2152 | +#endif |
| 2153 | + |
| 2154 | +struct Fts5MatchinfoCtx { |
| 2155 | + int nCol; /* Number of cols in FTS5 table */ |
| 2156 | + int nPhrase; /* Number of phrases in FTS5 query */ |
| 2157 | + char *zArg; /* nul-term'd copy of 2nd arg */ |
| 2158 | + int nRet; /* Number of elements in aRet[] */ |
| 2159 | + u32 *aRet; /* Array of 32-bit unsigned ints to return */ |
| 2160 | +}; |
| 2161 | + |
| 2162 | + |
| 2163 | +/* |
| 2164 | +** Return a pointer to the fts5_api pointer for database connection db. |
| 2165 | +** If an error occurs, return NULL and leave an error in the database |
| 2166 | +** handle (accessible using sqlite3_errcode()/errmsg()). |
| 2167 | +*/ |
| 2168 | +static int fts5_api_from_db(sqlite3 *db, fts5_api **ppApi){ |
| 2169 | + sqlite3_stmt *pStmt = 0; |
| 2170 | + int rc; |
| 2171 | + |
| 2172 | + *ppApi = 0; |
| 2173 | + rc = sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0); |
| 2174 | + if( rc==SQLITE_OK ){ |
| 2175 | + sqlite3_bind_pointer(pStmt, 1, (void*)ppApi, "fts5_api_ptr", 0); |
| 2176 | + (void)sqlite3_step(pStmt); |
| 2177 | + rc = sqlite3_finalize(pStmt); |
| 2178 | + } |
| 2179 | + |
| 2180 | + return rc; |
| 2181 | +} |
| 2182 | + |
| 2183 | + |
| 2184 | +/* |
| 2185 | +** Argument f should be a flag accepted by matchinfo() (a valid character |
| 2186 | +** in the string passed as the second argument). If it is not, -1 is |
| 2187 | +** returned. Otherwise, if f is a valid matchinfo flag, the value returned |
| 2188 | +** is the number of 32-bit integers added to the output array if the |
| 2189 | +** table has nCol columns and the query nPhrase phrases. |
| 2190 | +*/ |
| 2191 | +static int fts5MatchinfoFlagsize(int nCol, int nPhrase, char f){ |
| 2192 | + int ret = -1; |
| 2193 | + switch( f ){ |
| 2194 | + case 'p': ret = 1; break; |
| 2195 | + case 'c': ret = 1; break; |
| 2196 | + case 'x': ret = 3 * nCol * nPhrase; break; |
| 2197 | + case 'y': ret = nCol * nPhrase; break; |
| 2198 | + case 'b': ret = ((nCol + 31) / 32) * nPhrase; break; |
| 2199 | + case 'n': ret = 1; break; |
| 2200 | + case 'a': ret = nCol; break; |
| 2201 | + case 'l': ret = nCol; break; |
| 2202 | + case 's': ret = nCol; break; |
| 2203 | + } |
| 2204 | + return ret; |
| 2205 | +} |
| 2206 | + |
| 2207 | +static int fts5MatchinfoIter( |
| 2208 | + const Fts5ExtensionApi *pApi, /* API offered by current FTS version */ |
| 2209 | + Fts5Context *pFts, /* First arg to pass to pApi functions */ |
| 2210 | + Fts5MatchinfoCtx *p, |
| 2211 | + int(*x)(const Fts5ExtensionApi*,Fts5Context*,Fts5MatchinfoCtx*,char,u32*) |
| 2212 | +){ |
| 2213 | + int i; |
| 2214 | + int n = 0; |
| 2215 | + int rc = SQLITE_OK; |
| 2216 | + char f; |
| 2217 | + for(i=0; (f = p->zArg[i]); i++){ |
| 2218 | + rc = x(pApi, pFts, p, f, &p->aRet[n]); |
| 2219 | + if( rc!=SQLITE_OK ) break; |
| 2220 | + n += fts5MatchinfoFlagsize(p->nCol, p->nPhrase, f); |
| 2221 | + } |
| 2222 | + return rc; |
| 2223 | +} |
| 2224 | + |
| 2225 | +static int fts5MatchinfoXCb( |
| 2226 | + const Fts5ExtensionApi *pApi, |
| 2227 | + Fts5Context *pFts, |
| 2228 | + void *pUserData |
| 2229 | +){ |
| 2230 | + Fts5PhraseIter iter; |
| 2231 | + int iCol, iOff; |
| 2232 | + u32 *aOut = (u32*)pUserData; |
| 2233 | + int iPrev = -1; |
| 2234 | + |
| 2235 | + for(pApi->xPhraseFirst(pFts, 0, &iter, &iCol, &iOff); |
| 2236 | + iCol>=0; |
| 2237 | + pApi->xPhraseNext(pFts, &iter, &iCol, &iOff) |
| 2238 | + ){ |
| 2239 | + aOut[iCol*3+1]++; |
| 2240 | + if( iCol!=iPrev ) aOut[iCol*3 + 2]++; |
| 2241 | + iPrev = iCol; |
| 2242 | + } |
| 2243 | + |
| 2244 | + return SQLITE_OK; |
| 2245 | +} |
| 2246 | + |
| 2247 | +static int fts5MatchinfoGlobalCb( |
| 2248 | + const Fts5ExtensionApi *pApi, |
| 2249 | + Fts5Context *pFts, |
| 2250 | + Fts5MatchinfoCtx *p, |
| 2251 | + char f, |
| 2252 | + u32 *aOut |
| 2253 | +){ |
| 2254 | + int rc = SQLITE_OK; |
| 2255 | + switch( f ){ |
| 2256 | + case 'p': |
| 2257 | + aOut[0] = p->nPhrase; |
| 2258 | + break; |
| 2259 | + |
| 2260 | + case 'c': |
| 2261 | + aOut[0] = p->nCol; |
| 2262 | + break; |
| 2263 | + |
| 2264 | + case 'x': { |
| 2265 | + int i; |
| 2266 | + for(i=0; i<p->nPhrase && rc==SQLITE_OK; i++){ |
| 2267 | + void *pPtr = (void*)&aOut[i * p->nCol * 3]; |
| 2268 | + rc = pApi->xQueryPhrase(pFts, i, pPtr, fts5MatchinfoXCb); |
| 2269 | + } |
| 2270 | + break; |
| 2271 | + } |
| 2272 | + |
| 2273 | + case 'n': { |
| 2274 | + sqlite3_int64 nRow; |
| 2275 | + rc = pApi->xRowCount(pFts, &nRow); |
| 2276 | + aOut[0] = (u32)nRow; |
| 2277 | + break; |
| 2278 | + } |
| 2279 | + |
| 2280 | + case 'a': { |
| 2281 | + sqlite3_int64 nRow = 0; |
| 2282 | + rc = pApi->xRowCount(pFts, &nRow); |
| 2283 | + if( nRow==0 ){ |
| 2284 | + memset(aOut, 0, sizeof(u32) * p->nCol); |
| 2285 | + }else{ |
| 2286 | + int i; |
| 2287 | + for(i=0; rc==SQLITE_OK && i<p->nCol; i++){ |
| 2288 | + sqlite3_int64 nToken; |
| 2289 | + rc = pApi->xColumnTotalSize(pFts, i, &nToken); |
| 2290 | + if( rc==SQLITE_OK){ |
| 2291 | + aOut[i] = (u32)((2*nToken + nRow) / (2*nRow)); |
| 2292 | + } |
| 2293 | + } |
| 2294 | + } |
| 2295 | + break; |
| 2296 | + } |
| 2297 | + |
| 2298 | + } |
| 2299 | + return rc; |
| 2300 | +} |
| 2301 | + |
| 2302 | +static int fts5MatchinfoLocalCb( |
| 2303 | + const Fts5ExtensionApi *pApi, |
| 2304 | + Fts5Context *pFts, |
| 2305 | + Fts5MatchinfoCtx *p, |
| 2306 | + char f, |
| 2307 | + u32 *aOut |
| 2308 | +){ |
| 2309 | + int i; |
| 2310 | + int rc = SQLITE_OK; |
| 2311 | + |
| 2312 | + switch( f ){ |
| 2313 | + case 'b': { |
| 2314 | + int iPhrase; |
| 2315 | + int nInt = ((p->nCol + 31) / 32) * p->nPhrase; |
| 2316 | + for(i=0; i<nInt; i++) aOut[i] = 0; |
| 2317 | + |
| 2318 | + for(iPhrase=0; iPhrase<p->nPhrase; iPhrase++){ |
| 2319 | + Fts5PhraseIter iter; |
| 2320 | + int iCol; |
| 2321 | + for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol); |
| 2322 | + iCol>=0; |
| 2323 | + pApi->xPhraseNextColumn(pFts, &iter, &iCol) |
| 2324 | + ){ |
| 2325 | + aOut[iPhrase * ((p->nCol+31)/32) + iCol/32] |= ((u32)1 << iCol%32); |
| 2326 | + } |
| 2327 | + } |
| 2328 | + |
| 2329 | + break; |
| 2330 | + } |
| 2331 | + |
| 2332 | + case 'x': |
| 2333 | + case 'y': { |
| 2334 | + int nMul = (f=='x' ? 3 : 1); |
| 2335 | + int iPhrase; |
| 2336 | + |
| 2337 | + for(i=0; i<(p->nCol*p->nPhrase); i++) aOut[i*nMul] = 0; |
| 2338 | + |
| 2339 | + for(iPhrase=0; iPhrase<p->nPhrase; iPhrase++){ |
| 2340 | + Fts5PhraseIter iter; |
| 2341 | + int iOff, iCol; |
| 2342 | + for(pApi->xPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff); |
| 2343 | + iOff>=0; |
| 2344 | + pApi->xPhraseNext(pFts, &iter, &iCol, &iOff) |
| 2345 | + ){ |
| 2346 | + aOut[nMul * (iCol + iPhrase * p->nCol)]++; |
| 2347 | + } |
| 2348 | + } |
| 2349 | + |
| 2350 | + break; |
| 2351 | + } |
| 2352 | + |
| 2353 | + case 'l': { |
| 2354 | + for(i=0; rc==SQLITE_OK && i<p->nCol; i++){ |
| 2355 | + int nToken; |
| 2356 | + rc = pApi->xColumnSize(pFts, i, &nToken); |
| 2357 | + aOut[i] = (u32)nToken; |
| 2358 | + } |
| 2359 | + break; |
| 2360 | + } |
| 2361 | + |
| 2362 | + case 's': { |
| 2363 | + int nInst; |
| 2364 | + |
| 2365 | + memset(aOut, 0, sizeof(u32) * p->nCol); |
| 2366 | + |
| 2367 | + rc = pApi->xInstCount(pFts, &nInst); |
| 2368 | + for(i=0; rc==SQLITE_OK && i<nInst; i++){ |
| 2369 | + int iPhrase, iOff, iCol = 0; |
| 2370 | + int iNextPhrase; |
| 2371 | + int iNextOff; |
| 2372 | + u32 nSeq = 1; |
| 2373 | + int j; |
| 2374 | + |
| 2375 | + rc = pApi->xInst(pFts, i, &iPhrase, &iCol, &iOff); |
| 2376 | + iNextPhrase = iPhrase+1; |
| 2377 | + iNextOff = iOff+pApi->xPhraseSize(pFts, 0); |
| 2378 | + for(j=i+1; rc==SQLITE_OK && j<nInst; j++){ |
| 2379 | + int ip, ic, io; |
| 2380 | + rc = pApi->xInst(pFts, j, &ip, &ic, &io); |
| 2381 | + if( ic!=iCol || io>iNextOff ) break; |
| 2382 | + if( ip==iNextPhrase && io==iNextOff ){ |
| 2383 | + nSeq++; |
| 2384 | + iNextPhrase = ip+1; |
| 2385 | + iNextOff = io + pApi->xPhraseSize(pFts, ip); |
| 2386 | + } |
| 2387 | + } |
| 2388 | + |
| 2389 | + if( nSeq>aOut[iCol] ) aOut[iCol] = nSeq; |
| 2390 | + } |
| 2391 | + |
| 2392 | + break; |
| 2393 | + } |
| 2394 | + } |
| 2395 | + return rc; |
| 2396 | +} |
| 2397 | + |
| 2398 | +static Fts5MatchinfoCtx *fts5MatchinfoNew( |
| 2399 | + const Fts5ExtensionApi *pApi, /* API offered by current FTS version */ |
| 2400 | + Fts5Context *pFts, /* First arg to pass to pApi functions */ |
| 2401 | + sqlite3_context *pCtx, /* Context for returning error message */ |
| 2402 | + const char *zArg /* Matchinfo flag string */ |
| 2403 | +){ |
| 2404 | + Fts5MatchinfoCtx *p; |
| 2405 | + int nCol; |
| 2406 | + int nPhrase; |
| 2407 | + int i; |
| 2408 | + int nInt; |
| 2409 | + sqlite3_int64 nByte; |
| 2410 | + int rc; |
| 2411 | + |
| 2412 | + nCol = pApi->xColumnCount(pFts); |
| 2413 | + nPhrase = pApi->xPhraseCount(pFts); |
| 2414 | + |
| 2415 | + nInt = 0; |
| 2416 | + for(i=0; zArg[i]; i++){ |
| 2417 | + int n = fts5MatchinfoFlagsize(nCol, nPhrase, zArg[i]); |
| 2418 | + if( n<0 ){ |
| 2419 | + char *zErr = sqlite3_mprintf("unrecognized matchinfo flag: %c", zArg[i]); |
| 2420 | + sqlite3_result_error(pCtx, zErr, -1); |
| 2421 | + sqlite3_free(zErr); |
| 2422 | + return 0; |
| 2423 | + } |
| 2424 | + nInt += n; |
| 2425 | + } |
| 2426 | + |
| 2427 | + nByte = sizeof(Fts5MatchinfoCtx) /* The struct itself */ |
| 2428 | + + sizeof(u32) * nInt /* The p->aRet[] array */ |
| 2429 | + + (i+1); /* The p->zArg string */ |
| 2430 | + p = (Fts5MatchinfoCtx*)sqlite3_malloc64(nByte); |
| 2431 | + if( p==0 ){ |
| 2432 | + sqlite3_result_error_nomem(pCtx); |
| 2433 | + return 0; |
| 2434 | + } |
| 2435 | + memset(p, 0, nByte); |
| 2436 | + |
| 2437 | + p->nCol = nCol; |
| 2438 | + p->nPhrase = nPhrase; |
| 2439 | + p->aRet = (u32*)&p[1]; |
| 2440 | + p->nRet = nInt; |
| 2441 | + p->zArg = (char*)&p->aRet[nInt]; |
| 2442 | + memcpy(p->zArg, zArg, i); |
| 2443 | + |
| 2444 | + rc = fts5MatchinfoIter(pApi, pFts, p, fts5MatchinfoGlobalCb); |
| 2445 | + if( rc!=SQLITE_OK ){ |
| 2446 | + sqlite3_result_error_code(pCtx, rc); |
| 2447 | + sqlite3_free(p); |
| 2448 | + p = 0; |
| 2449 | + } |
| 2450 | + |
| 2451 | + return p; |
| 2452 | +} |
| 2453 | + |
| 2454 | +static void fts5MatchinfoFunc( |
| 2455 | + const Fts5ExtensionApi *pApi, /* API offered by current FTS version */ |
| 2456 | + Fts5Context *pFts, /* First arg to pass to pApi functions */ |
| 2457 | + sqlite3_context *pCtx, /* Context for returning result/error */ |
| 2458 | + int nVal, /* Number of values in apVal[] array */ |
| 2459 | + sqlite3_value **apVal /* Array of trailing arguments */ |
| 2460 | +){ |
| 2461 | + const char *zArg; |
| 2462 | + Fts5MatchinfoCtx *p; |
| 2463 | + int rc = SQLITE_OK; |
| 2464 | + |
| 2465 | + if( nVal>0 ){ |
| 2466 | + zArg = (const char*)sqlite3_value_text(apVal[0]); |
| 2467 | + }else{ |
| 2468 | + zArg = "pcx"; |
| 2469 | + } |
| 2470 | + |
| 2471 | + p = (Fts5MatchinfoCtx*)pApi->xGetAuxdata(pFts, 0); |
| 2472 | + if( p==0 || sqlite3_stricmp(zArg, p->zArg) ){ |
| 2473 | + p = fts5MatchinfoNew(pApi, pFts, pCtx, zArg); |
| 2474 | + if( p==0 ){ |
| 2475 | + rc = SQLITE_NOMEM; |
| 2476 | + }else{ |
| 2477 | + rc = pApi->xSetAuxdata(pFts, p, sqlite3_free); |
| 2478 | + } |
| 2479 | + } |
| 2480 | + |
| 2481 | + if( rc==SQLITE_OK ){ |
| 2482 | + rc = fts5MatchinfoIter(pApi, pFts, p, fts5MatchinfoLocalCb); |
| 2483 | + } |
| 2484 | + if( rc!=SQLITE_OK ){ |
| 2485 | + sqlite3_result_error_code(pCtx, rc); |
| 2486 | + }else{ |
| 2487 | + /* No errors has occured, so return a copy of the array of integers. */ |
| 2488 | + int nByte = p->nRet * sizeof(u32); |
| 2489 | + sqlite3_result_blob(pCtx, (void*)p->aRet, nByte, SQLITE_TRANSIENT); |
| 2490 | + } |
| 2491 | +} |
| 2492 | + |
| 2493 | +int db_register_fts5(sqlite3 *db){ |
| 2494 | + int rc; /* Return code */ |
| 2495 | + fts5_api *pApi; /* FTS5 API functions */ |
| 2496 | + |
| 2497 | + /* Extract the FTS5 API pointer from the database handle. The |
| 2498 | + ** fts5_api_from_db() function above is copied verbatim from the |
| 2499 | + ** FTS5 documentation. Refer there for details. */ |
| 2500 | + rc = fts5_api_from_db(db, &pApi); |
| 2501 | + if( rc!=SQLITE_OK ) return rc; |
| 2502 | + |
| 2503 | + /* If fts5_api_from_db() returns NULL, then either FTS5 is not registered |
| 2504 | + ** with this database handle, or an error (OOM perhaps?) has occurred. |
| 2505 | + ** |
| 2506 | + ** Also check that the fts5_api object is version 2 or newer. |
| 2507 | + */ |
| 2508 | + if( pApi==0 || pApi->iVersion<2 ){ |
| 2509 | + return SQLITE_ERROR; |
| 2510 | + } |
| 2511 | + |
| 2512 | + /* Register the implementation of matchinfo() */ |
| 2513 | + rc = pApi->xCreateFunction(pApi, "matchinfo", 0, fts5MatchinfoFunc, 0); |
| 2514 | + |
| 2515 | + return rc; |
| 2516 | +} |
| 2108 | 2517 | |