Fossil SCM

Add the ability to do search using an SQLite FTS4 index.

drh 2015-02-03 05:24 trunk merge
Commit a00a140bff46373e36855e2cb734abbc73fbb571
+10 -8
--- src/db.c
+++ src/db.c
@@ -547,12 +547,13 @@
547547
va_end(ap);
548548
z = blob_str(&sql);
549549
while( rc==SQLITE_OK && z[0] ){
550550
pStmt = 0;
551551
rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd);
552
- if( rc!=SQLITE_OK ) break;
553
- if( pStmt ){
552
+ if( rc ){
553
+ db_err("%s: {%s}", sqlite3_errmsg(g.db), z);
554
+ }else if( pStmt ){
554555
db.nPrepare++;
555556
while( sqlite3_step(pStmt)==SQLITE_ROW ){}
556557
rc = sqlite3_finalize(pStmt);
557558
if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z);
558559
}
@@ -1380,15 +1381,10 @@
13801381
while( db.pAllStmt ){
13811382
db_finalize(db.pAllStmt);
13821383
}
13831384
db_end_transaction(1);
13841385
pStmt = 0;
1385
- if( reportErrors ){
1386
- while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
1387
- fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
1388
- }
1389
- }
13901386
db_close_config();
13911387
13921388
/* If the localdb (the check-out database) is open and if it has
13931389
** a lot of unused free space, then VACUUM it as we shut down.
13941390
*/
@@ -1399,12 +1395,18 @@
13991395
db_multi_exec("VACUUM;");
14001396
}
14011397
}
14021398
14031399
if( g.db ){
1400
+ int rc;
14041401
sqlite3_wal_checkpoint(g.db, 0);
1405
- sqlite3_close(g.db);
1402
+ rc = sqlite3_close(g.db);
1403
+ if( rc==SQLITE_BUSY && reportErrors ){
1404
+ while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
1405
+ fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
1406
+ }
1407
+ }
14061408
g.db = 0;
14071409
g.zMainDbType = 0;
14081410
}
14091411
g.repositoryOpen = 0;
14101412
g.localOpen = 0;
14111413
--- src/db.c
+++ src/db.c
@@ -547,12 +547,13 @@
547 va_end(ap);
548 z = blob_str(&sql);
549 while( rc==SQLITE_OK && z[0] ){
550 pStmt = 0;
551 rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd);
552 if( rc!=SQLITE_OK ) break;
553 if( pStmt ){
 
554 db.nPrepare++;
555 while( sqlite3_step(pStmt)==SQLITE_ROW ){}
556 rc = sqlite3_finalize(pStmt);
557 if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z);
558 }
@@ -1380,15 +1381,10 @@
1380 while( db.pAllStmt ){
1381 db_finalize(db.pAllStmt);
1382 }
1383 db_end_transaction(1);
1384 pStmt = 0;
1385 if( reportErrors ){
1386 while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
1387 fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
1388 }
1389 }
1390 db_close_config();
1391
1392 /* If the localdb (the check-out database) is open and if it has
1393 ** a lot of unused free space, then VACUUM it as we shut down.
1394 */
@@ -1399,12 +1395,18 @@
1399 db_multi_exec("VACUUM;");
1400 }
1401 }
1402
1403 if( g.db ){
 
1404 sqlite3_wal_checkpoint(g.db, 0);
1405 sqlite3_close(g.db);
 
 
 
 
 
1406 g.db = 0;
1407 g.zMainDbType = 0;
1408 }
1409 g.repositoryOpen = 0;
1410 g.localOpen = 0;
1411
--- src/db.c
+++ src/db.c
@@ -547,12 +547,13 @@
547 va_end(ap);
548 z = blob_str(&sql);
549 while( rc==SQLITE_OK && z[0] ){
550 pStmt = 0;
551 rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd);
552 if( rc ){
553 db_err("%s: {%s}", sqlite3_errmsg(g.db), z);
554 }else if( pStmt ){
555 db.nPrepare++;
556 while( sqlite3_step(pStmt)==SQLITE_ROW ){}
557 rc = sqlite3_finalize(pStmt);
558 if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z);
559 }
@@ -1380,15 +1381,10 @@
1381 while( db.pAllStmt ){
1382 db_finalize(db.pAllStmt);
1383 }
1384 db_end_transaction(1);
1385 pStmt = 0;
 
 
 
 
 
1386 db_close_config();
1387
1388 /* If the localdb (the check-out database) is open and if it has
1389 ** a lot of unused free space, then VACUUM it as we shut down.
1390 */
@@ -1399,12 +1395,18 @@
1395 db_multi_exec("VACUUM;");
1396 }
1397 }
1398
1399 if( g.db ){
1400 int rc;
1401 sqlite3_wal_checkpoint(g.db, 0);
1402 rc = sqlite3_close(g.db);
1403 if( rc==SQLITE_BUSY && reportErrors ){
1404 while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
1405 fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
1406 }
1407 }
1408 g.db = 0;
1409 g.zMainDbType = 0;
1410 }
1411 g.repositoryOpen = 0;
1412 g.localOpen = 0;
1413
+2 -2
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
4444
const char *zSep = "(";
4545
int nTerm = 0;
4646
int i;
4747
int cTerm;
4848
49
- if( zGlobList==0 || zGlobList[0]==0 ) return "0";
49
+ if( zGlobList==0 || zGlobList[0]==0 ) return fossil_strdup("0");
5050
blob_zero(&expr);
5151
while( zGlobList[0] ){
5252
while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
5353
zGlobList++; /* Skip leading commas, spaces, and newlines */
5454
}
@@ -73,11 +73,11 @@
7373
}
7474
if( nTerm ){
7575
blob_appendf(&expr, ")");
7676
return blob_str(&expr);
7777
}else{
78
- return "0";
78
+ return fossil_strdup("0");
7979
}
8080
}
8181
8282
#if INTERFACE
8383
/*
8484
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
44 const char *zSep = "(";
45 int nTerm = 0;
46 int i;
47 int cTerm;
48
49 if( zGlobList==0 || zGlobList[0]==0 ) return "0";
50 blob_zero(&expr);
51 while( zGlobList[0] ){
52 while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
53 zGlobList++; /* Skip leading commas, spaces, and newlines */
54 }
@@ -73,11 +73,11 @@
73 }
74 if( nTerm ){
75 blob_appendf(&expr, ")");
76 return blob_str(&expr);
77 }else{
78 return "0";
79 }
80 }
81
82 #if INTERFACE
83 /*
84
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
44 const char *zSep = "(";
45 int nTerm = 0;
46 int i;
47 int cTerm;
48
49 if( zGlobList==0 || zGlobList[0]==0 ) return fossil_strdup("0");
50 blob_zero(&expr);
51 while( zGlobList[0] ){
52 while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
53 zGlobList++; /* Skip leading commas, spaces, and newlines */
54 }
@@ -73,11 +73,11 @@
73 }
74 if( nTerm ){
75 blob_appendf(&expr, ")");
76 return blob_str(&expr);
77 }else{
78 return fossil_strdup("0");
79 }
80 }
81
82 #if INTERFACE
83 /*
84
+2 -2
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
4444
const char *zSep = "(";
4545
int nTerm = 0;
4646
int i;
4747
int cTerm;
4848
49
- if( zGlobList==0 || zGlobList[0]==0 ) return "0";
49
+ if( zGlobList==0 || zGlobList[0]==0 ) return fossil_strdup("0");
5050
blob_zero(&expr);
5151
while( zGlobList[0] ){
5252
while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
5353
zGlobList++; /* Skip leading commas, spaces, and newlines */
5454
}
@@ -73,11 +73,11 @@
7373
}
7474
if( nTerm ){
7575
blob_appendf(&expr, ")");
7676
return blob_str(&expr);
7777
}else{
78
- return "0";
78
+ return fossil_strdup("0");
7979
}
8080
}
8181
8282
#if INTERFACE
8383
/*
8484
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
44 const char *zSep = "(";
45 int nTerm = 0;
46 int i;
47 int cTerm;
48
49 if( zGlobList==0 || zGlobList[0]==0 ) return "0";
50 blob_zero(&expr);
51 while( zGlobList[0] ){
52 while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
53 zGlobList++; /* Skip leading commas, spaces, and newlines */
54 }
@@ -73,11 +73,11 @@
73 }
74 if( nTerm ){
75 blob_appendf(&expr, ")");
76 return blob_str(&expr);
77 }else{
78 return "0";
79 }
80 }
81
82 #if INTERFACE
83 /*
84
--- src/glob.c
+++ src/glob.c
@@ -44,11 +44,11 @@
44 const char *zSep = "(";
45 int nTerm = 0;
46 int i;
47 int cTerm;
48
49 if( zGlobList==0 || zGlobList[0]==0 ) return fossil_strdup("0");
50 blob_zero(&expr);
51 while( zGlobList[0] ){
52 while( fossil_isspace(zGlobList[0]) || zGlobList[0]==',' ){
53 zGlobList++; /* Skip leading commas, spaces, and newlines */
54 }
@@ -73,11 +73,11 @@
73 }
74 if( nTerm ){
75 blob_appendf(&expr, ")");
76 return blob_str(&expr);
77 }else{
78 return fossil_strdup("0");
79 }
80 }
81
82 #if INTERFACE
83 /*
84
+2 -1
--- src/main.mk
+++ src/main.mk
@@ -449,11 +449,12 @@
449449
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
450450
-DSQLITE_ENABLE_LOCKING_STYLE=0 \
451451
-DSQLITE_THREADSAFE=0 \
452452
-DSQLITE_DEFAULT_FILE_FORMAT=4 \
453453
-DSQLITE_OMIT_DEPRECATED \
454
- -DSQLITE_ENABLE_EXPLAIN_COMMENTS
454
+ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
455
+ -DSQLITE_ENABLE_FTS4
455456
456457
# Setup the options used to compile the included SQLite shell.
457458
SHELL_OPTIONS = -Dmain=sqlite3_shell \
458459
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
459460
-DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
460461
--- src/main.mk
+++ src/main.mk
@@ -449,11 +449,12 @@
449 -DSQLITE_OMIT_LOAD_EXTENSION=1 \
450 -DSQLITE_ENABLE_LOCKING_STYLE=0 \
451 -DSQLITE_THREADSAFE=0 \
452 -DSQLITE_DEFAULT_FILE_FORMAT=4 \
453 -DSQLITE_OMIT_DEPRECATED \
454 -DSQLITE_ENABLE_EXPLAIN_COMMENTS
 
455
456 # Setup the options used to compile the included SQLite shell.
457 SHELL_OPTIONS = -Dmain=sqlite3_shell \
458 -DSQLITE_OMIT_LOAD_EXTENSION=1 \
459 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
460
--- src/main.mk
+++ src/main.mk
@@ -449,11 +449,12 @@
449 -DSQLITE_OMIT_LOAD_EXTENSION=1 \
450 -DSQLITE_ENABLE_LOCKING_STYLE=0 \
451 -DSQLITE_THREADSAFE=0 \
452 -DSQLITE_DEFAULT_FILE_FORMAT=4 \
453 -DSQLITE_OMIT_DEPRECATED \
454 -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
455 -DSQLITE_ENABLE_FTS4
456
457 # Setup the options used to compile the included SQLite shell.
458 SHELL_OPTIONS = -Dmain=sqlite3_shell \
459 -DSQLITE_OMIT_LOAD_EXTENSION=1 \
460 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
461
--- src/makemake.tcl
+++ src/makemake.tcl
@@ -157,10 +157,11 @@
157157
-DSQLITE_ENABLE_LOCKING_STYLE=0
158158
-DSQLITE_THREADSAFE=0
159159
-DSQLITE_DEFAULT_FILE_FORMAT=4
160160
-DSQLITE_OMIT_DEPRECATED
161161
-DSQLITE_ENABLE_EXPLAIN_COMMENTS
162
+ -DSQLITE_ENABLE_FTS4
162163
}
163164
#lappend SQLITE_OPTIONS -DSQLITE_ENABLE_FTS3=1
164165
#lappend SQLITE_OPTIONS -DSQLITE_ENABLE_STAT4
165166
#lappend SQLITE_OPTIONS -DSQLITE_WIN32_NO_ANSI
166167
#lappend SQLITE_OPTIONS -DSQLITE_WINNT_MAX_PATH_CHARS=4096
167168
--- src/makemake.tcl
+++ src/makemake.tcl
@@ -157,10 +157,11 @@
157 -DSQLITE_ENABLE_LOCKING_STYLE=0
158 -DSQLITE_THREADSAFE=0
159 -DSQLITE_DEFAULT_FILE_FORMAT=4
160 -DSQLITE_OMIT_DEPRECATED
161 -DSQLITE_ENABLE_EXPLAIN_COMMENTS
 
162 }
163 #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_FTS3=1
164 #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_STAT4
165 #lappend SQLITE_OPTIONS -DSQLITE_WIN32_NO_ANSI
166 #lappend SQLITE_OPTIONS -DSQLITE_WINNT_MAX_PATH_CHARS=4096
167
--- src/makemake.tcl
+++ src/makemake.tcl
@@ -157,10 +157,11 @@
157 -DSQLITE_ENABLE_LOCKING_STYLE=0
158 -DSQLITE_THREADSAFE=0
159 -DSQLITE_DEFAULT_FILE_FORMAT=4
160 -DSQLITE_OMIT_DEPRECATED
161 -DSQLITE_ENABLE_EXPLAIN_COMMENTS
162 -DSQLITE_ENABLE_FTS4
163 }
164 #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_FTS3=1
165 #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_STAT4
166 #lappend SQLITE_OPTIONS -DSQLITE_WIN32_NO_ANSI
167 #lappend SQLITE_OPTIONS -DSQLITE_WINNT_MAX_PATH_CHARS=4096
168
--- src/manifest.c
+++ src/manifest.c
@@ -1829,10 +1829,11 @@
18291829
for(i=0; i<p->nFile; i++){
18301830
add_one_mlink(0, 0, rid, p->aFile[i].zUuid, p->aFile[i].zName, 0,
18311831
isPublic, 1, manifest_file_mperm(&p->aFile[i]));
18321832
}
18331833
}
1834
+ search_doc_touch('c', rid, 0);
18341835
db_multi_exec(
18351836
"REPLACE INTO event(type,mtime,objid,user,comment,"
18361837
"bgcolor,euser,ecomment,omtime)"
18371838
"VALUES('ci',"
18381839
" coalesce("
@@ -1934,10 +1935,11 @@
19341935
if( nWiki>0 ){
19351936
zComment = mprintf("Changes to wiki page [%h]", p->zWikiTitle);
19361937
}else{
19371938
zComment = mprintf("Deleted wiki page [%h]", p->zWikiTitle);
19381939
}
1940
+ search_doc_touch('w',rid,p->zWikiTitle);
19391941
db_multi_exec(
19401942
"REPLACE INTO event(type,mtime,objid,user,comment,"
19411943
" bgcolor,euser,ecomment)"
19421944
"VALUES('w',%.17g,%d,%Q,%Q,"
19431945
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>1),"
@@ -1986,10 +1988,11 @@
19861988
}
19871989
}
19881990
if( subsequent ){
19891991
content_deltify(rid, subsequent, 0);
19901992
}else{
1993
+ search_doc_touch('e',rid,0);
19911994
db_multi_exec(
19921995
"REPLACE INTO event(type,mtime,objid,tagid,user,comment,bgcolor)"
19931996
"VALUES('e',%.17g,%d,%d,%Q,%Q,"
19941997
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d));",
19951998
p->rEventDate, rid, tagid, p->zUser, p->zComment,
19961999
--- src/manifest.c
+++ src/manifest.c
@@ -1829,10 +1829,11 @@
1829 for(i=0; i<p->nFile; i++){
1830 add_one_mlink(0, 0, rid, p->aFile[i].zUuid, p->aFile[i].zName, 0,
1831 isPublic, 1, manifest_file_mperm(&p->aFile[i]));
1832 }
1833 }
 
1834 db_multi_exec(
1835 "REPLACE INTO event(type,mtime,objid,user,comment,"
1836 "bgcolor,euser,ecomment,omtime)"
1837 "VALUES('ci',"
1838 " coalesce("
@@ -1934,10 +1935,11 @@
1934 if( nWiki>0 ){
1935 zComment = mprintf("Changes to wiki page [%h]", p->zWikiTitle);
1936 }else{
1937 zComment = mprintf("Deleted wiki page [%h]", p->zWikiTitle);
1938 }
 
1939 db_multi_exec(
1940 "REPLACE INTO event(type,mtime,objid,user,comment,"
1941 " bgcolor,euser,ecomment)"
1942 "VALUES('w',%.17g,%d,%Q,%Q,"
1943 " (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>1),"
@@ -1986,10 +1988,11 @@
1986 }
1987 }
1988 if( subsequent ){
1989 content_deltify(rid, subsequent, 0);
1990 }else{
 
1991 db_multi_exec(
1992 "REPLACE INTO event(type,mtime,objid,tagid,user,comment,bgcolor)"
1993 "VALUES('e',%.17g,%d,%d,%Q,%Q,"
1994 " (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d));",
1995 p->rEventDate, rid, tagid, p->zUser, p->zComment,
1996
--- src/manifest.c
+++ src/manifest.c
@@ -1829,10 +1829,11 @@
1829 for(i=0; i<p->nFile; i++){
1830 add_one_mlink(0, 0, rid, p->aFile[i].zUuid, p->aFile[i].zName, 0,
1831 isPublic, 1, manifest_file_mperm(&p->aFile[i]));
1832 }
1833 }
1834 search_doc_touch('c', rid, 0);
1835 db_multi_exec(
1836 "REPLACE INTO event(type,mtime,objid,user,comment,"
1837 "bgcolor,euser,ecomment,omtime)"
1838 "VALUES('ci',"
1839 " coalesce("
@@ -1934,10 +1935,11 @@
1935 if( nWiki>0 ){
1936 zComment = mprintf("Changes to wiki page [%h]", p->zWikiTitle);
1937 }else{
1938 zComment = mprintf("Deleted wiki page [%h]", p->zWikiTitle);
1939 }
1940 search_doc_touch('w',rid,p->zWikiTitle);
1941 db_multi_exec(
1942 "REPLACE INTO event(type,mtime,objid,user,comment,"
1943 " bgcolor,euser,ecomment)"
1944 "VALUES('w',%.17g,%d,%Q,%Q,"
1945 " (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>1),"
@@ -1986,10 +1988,11 @@
1988 }
1989 }
1990 if( subsequent ){
1991 content_deltify(rid, subsequent, 0);
1992 }else{
1993 search_doc_touch('e',rid,0);
1994 db_multi_exec(
1995 "REPLACE INTO event(type,mtime,objid,tagid,user,comment,bgcolor)"
1996 "VALUES('e',%.17g,%d,%d,%Q,%Q,"
1997 " (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d));",
1998 p->rEventDate, rid, tagid, p->zUser, p->zComment,
1999
+617 -179
--- src/search.c
+++ src/search.c
@@ -45,14 +45,15 @@
4545
char *zPattern; /* The search pattern */
4646
char *zMarkBegin; /* Start of a match */
4747
char *zMarkEnd; /* End of a match */
4848
char *zMarkGap; /* A gap between two matches */
4949
unsigned fSrchFlg; /* Flags */
50
+ int iScore; /* Score of the last match attempt */
51
+ Blob snip; /* Snippet for the most recent match */
5052
};
5153
5254
#define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */
53
-#define SRCHFLG_SCORE 0x02 /* Prepend the score to each snippet */
5455
#define SRCHFLG_STATIC 0x04 /* The static gSearch object */
5556
5657
#endif
5758
5859
/*
@@ -92,10 +93,11 @@
9293
if( p ){
9394
fossil_free(p->zPattern);
9495
fossil_free(p->zMarkBegin);
9596
fossil_free(p->zMarkEnd);
9697
fossil_free(p->zMarkGap);
98
+ if( p->iScore ) blob_reset(&p->snip);
9799
memset(p, 0, sizeof(*p));
98100
if( p!=&gSearch ) fossil_free(p);
99101
}
100102
}
101103
@@ -123,10 +125,11 @@
123125
p->zPattern = z = mprintf("%s", zPattern);
124126
p->zMarkBegin = mprintf("%s", zMarkBegin);
125127
p->zMarkEnd = mprintf("%s", zMarkEnd);
126128
p->zMarkGap = mprintf("%s", zMarkGap);
127129
p->fSrchFlg = fSrchFlg;
130
+ blob_init(&p->snip, 0, 0);
128131
while( *z && p->nTerm<SEARCH_MAX_TERM ){
129132
while( *z && !ISALNUM(*z) ){ z++; }
130133
if( *z==0 ) break;
131134
p->a[p->nTerm].z = z;
132135
for(i=1; ISALNUM(z[i]); i++){}
@@ -156,24 +159,26 @@
156159
}
157160
}
158161
159162
/*
160163
** Compare a search pattern against one or more input strings which
161
-** collectively comprise a document. Return a match score. Optionally
162
-** also return a "snippet".
164
+** collectively comprise a document. Return a match score. Any
165
+** postive value means there was a match. Zero means that one or
166
+** more terms are missing.
167
+**
168
+** The score and a snippet are record for future use.
163169
**
164170
** Scoring:
165171
** * All terms must match at least once or the score is zero
166172
** * One point for each matching term
167173
** * Extra points if consecutive words of the pattern are consecutive
168174
** in the document
169175
*/
170
-static int search_score(
176
+static int search_match(
171177
Search *p, /* Search pattern and flags */
172178
int nDoc, /* Number of strings in this document */
173
- const char **azDoc, /* Text of each string */
174
- Blob *pSnip /* If not NULL: Write a snippet here */
179
+ const char **azDoc /* Text of each string */
175180
){
176181
int score; /* Final score */
177182
int i; /* Offset into current document */
178183
int ii; /* Loop counter */
179184
int j; /* Loop over search terms */
@@ -226,18 +231,17 @@
226231
/* Finished search all documents.
227232
** Every term must be seen or else the score is zero
228233
*/
229234
score = 1;
230235
for(j=0; j<p->nTerm; j++) score *= anMatch[j];
231
- if( score==0 || pSnip==0 ) return score;
236
+ blob_reset(&p->snip);
237
+ p->iScore = score;
238
+ if( score==0 ) return score;
232239
233240
234241
/* Prepare a snippet that describes the matching text.
235242
*/
236
- blob_init(pSnip, 0, 0);
237
- if( p->fSrchFlg & SRCHFLG_SCORE ) blob_appendf(pSnip, "%08x", score);
238
-
239243
while(1){
240244
int iOfst;
241245
int iTail;
242246
int iBest;
243247
for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){}
@@ -272,11 +276,11 @@
272276
if( iOfst<0 ) iOfst = 0;
273277
while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--;
274278
while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++;
275279
for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){}
276280
while( ISALNUM(zDoc[iTail]) ) iTail++;
277
- if( iOfst>0 || wantGap ) blob_append(pSnip, p->zMarkGap, -1);
281
+ if( iOfst>0 || wantGap ) blob_append(&p->snip, p->zMarkGap, -1);
278282
wantGap = zDoc[iTail]!=0;
279283
zDoc += iOfst;
280284
iTail -= iOfst;
281285
282286
/* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */
@@ -285,53 +289,51 @@
285289
for(j=0; j<p->nTerm; j++){
286290
int n = p->a[j].n;
287291
if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0
288292
&& (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*')
289293
){
290
- snippet_text_append(p, pSnip, zDoc, i);
294
+ snippet_text_append(p, &p->snip, zDoc, i);
291295
zDoc += i;
292296
iTail -= i;
293
- blob_append(pSnip, p->zMarkBegin, -1);
297
+ blob_append(&p->snip, p->zMarkBegin, -1);
294298
if( p->a[j].z[n]=='*' ){
295299
while( ISALNUM(zDoc[n]) ) n++;
296300
}
297
- snippet_text_append(p, pSnip, zDoc, n);
301
+ snippet_text_append(p, &p->snip, zDoc, n);
298302
zDoc += n;
299303
iTail -= n;
300
- blob_append(pSnip, p->zMarkEnd, -1);
304
+ blob_append(&p->snip, p->zMarkEnd, -1);
301305
i = -1;
302306
break;
303307
} /* end-if */
304308
} /* end for(j) */
305309
if( j<p->nTerm ){
306310
while( ISALNUM(zDoc[i]) && i<iTail ){ i++; }
307311
}
308312
} /* end for(i) */
309
- snippet_text_append(p, pSnip, zDoc, iTail);
313
+ snippet_text_append(p, &p->snip, zDoc, iTail);
310314
}
311
- if( wantGap ) blob_append(pSnip, p->zMarkGap, -1);
315
+ if( wantGap ) blob_append(&p->snip, p->zMarkGap, -1);
312316
return score;
313317
}
314318
315319
/*
316
-** COMMAND: test-snippet
320
+** COMMAND: test-match
317321
**
318
-** Usage: fossil test-snippet SEARCHSTRING FILE1 FILE2 ...
322
+** Usage: fossil test-match SEARCHSTRING FILE1 FILE2 ...
319323
*/
320
-void test_snippet_cmd(void){
324
+void test_match_cmd(void){
321325
Search *p;
322326
int i;
323327
Blob x;
324
- Blob snip;
325328
int score;
326329
char *zDoc;
327330
int flg = 0;
328331
char *zBegin = (char*)find_option("begin",0,1);
329332
char *zEnd = (char*)find_option("end",0,1);
330333
char *zGap = (char*)find_option("gap",0,1);
331334
if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML;
332
- if( find_option("score",0,0)!=0 ) flg |= SRCHFLG_SCORE;
333335
if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC;
334336
verify_all_options();
335337
if( g.argc<4 ) usage("SEARCHSTRING FILE1...");
336338
if( zBegin==0 ) zBegin = "[[";
337339
if( zEnd==0 ) zEnd = "]]";
@@ -338,18 +340,18 @@
338340
if( zGap==0 ) zGap = " ... ";
339341
p = search_init(g.argv[2], zBegin, zEnd, zGap, flg);
340342
for(i=3; i<g.argc; i++){
341343
blob_read_from_file(&x, g.argv[i]);
342344
zDoc = blob_str(&x);
343
- score = search_score(p, 1, (const char**)&zDoc, &snip);
344
- fossil_print("%s: %d\n", g.argv[i], score);
345
+ score = search_match(p, 1, (const char**)&zDoc);
346
+ fossil_print("%s: %d\n", g.argv[i], p->iScore);
345347
blob_reset(&x);
346348
if( score ){
347
- fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&snip), '=');
348
- blob_reset(&snip);
349
+ fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&p->snip), '=');
349350
}
350351
}
352
+ search_end(p);
351353
}
352354
353355
/*
354356
** An SQL function to initialize the global search pattern:
355357
**
@@ -361,12 +363,12 @@
361363
sqlite3_context *context,
362364
int argc,
363365
sqlite3_value **argv
364366
){
365367
const char *zPattern = 0;
366
- const char *zBegin = "<b>";
367
- const char *zEnd = "</b>";
368
+ const char *zBegin = "<mark>";
369
+ const char *zEnd = "</mark>";
368370
const char *zGap = " ... ";
369371
unsigned int flg = SRCHFLG_HTML;
370372
switch( argc ){
371373
default:
372374
flg = (unsigned int)sqlite3_value_int(argv[4]);
@@ -385,35 +387,45 @@
385387
search_end(&gSearch);
386388
}
387389
}
388390
389391
/*
390
-** This is an SQLite function that scores its input using
391
-** the pattern from the previous call to search_init().
392
+** Try to match the input text against the search parameters set up
393
+** by the previous search_init() call. Remember the results globally.
394
+** Return non-zero on a match and zero on a miss.
395
+*/
396
+static void search_match_sqlfunc(
397
+ sqlite3_context *context,
398
+ int argc,
399
+ sqlite3_value **argv
400
+){
401
+ const char *zSText = (const char*)sqlite3_value_text(argv[0]);
402
+ int rc;
403
+ if( zSText==0 ) return;
404
+ rc = search_match(&gSearch, 1, &zSText);
405
+ sqlite3_result_int(context, rc);
406
+}
407
+
408
+/*
409
+** These SQL functions return the results of the last
410
+** call to the search_match() SQL function.
392411
*/
393412
static void search_score_sqlfunc(
394413
sqlite3_context *context,
395414
int argc,
396415
sqlite3_value **argv
397416
){
398
- int isSnippet = sqlite3_user_data(context)!=0;
399
- const char **azDoc;
400
- int score;
401
- int i;
402
- Blob snip;
403
-
404
- if( gSearch.nTerm==0 ) return;
405
- azDoc = fossil_malloc( sizeof(const char*)*(argc+1) );
406
- for(i=0; i<argc; i++) azDoc[i] = (const char*)sqlite3_value_text(argv[i]);
407
- score = search_score(&gSearch, argc, azDoc, isSnippet ? &snip : 0);
408
- fossil_free((void *)azDoc);
409
- if( isSnippet ){
410
- if( score ){
411
- sqlite3_result_text(context, blob_materialize(&snip), -1, fossil_free);
412
- }
413
- }else{
414
- sqlite3_result_int(context, score);
417
+ sqlite3_result_int(context, gSearch.iScore);
418
+}
419
+static void search_snippet_sqlfunc(
420
+ sqlite3_context *context,
421
+ int argc,
422
+ sqlite3_value **argv
423
+){
424
+ if( blob_size(&gSearch.snip)>0 ){
425
+ sqlite3_result_text(context, blob_str(&gSearch.snip), -1, fossil_free);
426
+ blob_init(&gSearch.snip, 0, 0);
415427
}
416428
}
417429
418430
/*
419431
** This is an SQLite function that computes the searchable text.
@@ -449,14 +461,18 @@
449461
** Register the "score()" SQL function to score its input text
450462
** using the given Search object. Once this function is registered,
451463
** do not delete the Search object.
452464
*/
453465
void search_sql_setup(sqlite3 *db){
454
- sqlite3_create_function(db, "score", -1, SQLITE_UTF8, 0,
455
- search_score_sqlfunc, 0, 0);
456
- sqlite3_create_function(db, "snippet", -1, SQLITE_UTF8, &gSearch,
466
+ static int once = 0;
467
+ if( once++ ) return;
468
+ sqlite3_create_function(db, "search_match", 1, SQLITE_UTF8, 0,
469
+ search_match_sqlfunc, 0, 0);
470
+ sqlite3_create_function(db, "search_score", 0, SQLITE_UTF8, 0,
457471
search_score_sqlfunc, 0, 0);
472
+ sqlite3_create_function(db, "search_snippet", 0, SQLITE_UTF8, 0,
473
+ search_snippet_sqlfunc, 0, 0);
458474
sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0,
459475
search_init_sqlfunc, 0, 0);
460476
sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0,
461477
search_stext_sqlfunc, 0, 0);
462478
sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0,
@@ -551,28 +567,217 @@
551567
/*
552568
** Remove bits from srchFlags which are disallowed by either the
553569
** current server configuration or by user permissions.
554570
*/
555571
unsigned int search_restrict(unsigned int srchFlags){
556
- if( (srchFlags & SRCH_CKIN)!=0
557
- && (g.perm.Read==0 || db_get_boolean("search-ci",0)==0) ){
572
+ if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC);
573
+ if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT);
574
+ if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI);
575
+ if( search_index_exists() ) return srchFlags;
576
+ if( (srchFlags & SRCH_CKIN)!=0 && db_get_boolean("search-ci",0)==0 ){
558577
srchFlags &= ~SRCH_CKIN;
559578
}
560
- if( (srchFlags & SRCH_DOC)!=0
561
- && (g.perm.Read==0 || db_get_boolean("search-doc",0)==0) ){
579
+ if( (srchFlags & SRCH_DOC)!=0 && db_get_boolean("search-doc",0)==0 ){
562580
srchFlags &= ~SRCH_DOC;
563581
}
564
- if( (srchFlags & SRCH_TKT)!=0
565
- && (g.perm.RdTkt==0 || db_get_boolean("search-tkt",0)==0) ){
582
+ if( (srchFlags & SRCH_TKT)!=0 && db_get_boolean("search-tkt",0)==0 ){
566583
srchFlags &= ~SRCH_TKT;
567584
}
568
- if( (srchFlags & SRCH_WIKI)!=0
569
- && (g.perm.RdWiki==0 || db_get_boolean("search-wiki",0)==0) ){
585
+ if( (srchFlags & SRCH_WIKI)!=0 && db_get_boolean("search-wiki",0)==0 ){
570586
srchFlags &= ~SRCH_WIKI;
571587
}
572588
return srchFlags;
573589
}
590
+
591
+/*
592
+** When this routine is called, there already exists a table
593
+**
594
+** x(label,url,score,date,snip).
595
+**
596
+** And the srchFlags parameter has been validated. This routine
597
+** fills the X table with search results using a full-text scan.
598
+**
599
+** The companion indexed scan routine is search_indexed().
600
+*/
601
+static void search_fullscan(
602
+ const char *zPattern, /* The query pattern */
603
+ unsigned int srchFlags /* What to search over */
604
+){
605
+ search_init(zPattern, "<b>", "</b>", " ... ",
606
+ SRCHFLG_STATIC|SRCHFLG_HTML);
607
+ if( (srchFlags & SRCH_DOC)!=0 ){
608
+ char *zDocGlob = db_get("doc-glob","");
609
+ char *zDocBr = db_get("doc-branch","trunk");
610
+ if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){
611
+ db_multi_exec(
612
+ "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
613
+ );
614
+ db_multi_exec(
615
+ "INSERT INTO x(label,url,score,date,snip)"
616
+ " SELECT printf('Document: %%s',foci.filename),"
617
+ " printf('%R/doc/%T/%%s',foci.filename),"
618
+ " search_score(),"
619
+ " (SELECT datetime(event.mtime) FROM event"
620
+ " WHERE objid=symbolic_name_to_rid('trunk')),"
621
+ " search_snippet()"
622
+ " FROM foci CROSS JOIN blob"
623
+ " WHERE checkinID=symbolic_name_to_rid('trunk')"
624
+ " AND blob.uuid=foci.uuid"
625
+ " AND search_match(stext('d',blob.rid,foci.filename))"
626
+ " AND %z",
627
+ zDocBr, glob_expr("foci.filename", zDocGlob)
628
+ );
629
+ }
630
+ }
631
+ if( (srchFlags & SRCH_WIKI)!=0 ){
632
+ db_multi_exec(
633
+ "WITH wiki(name,rid,mtime) AS ("
634
+ " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)"
635
+ " FROM tag, tagxref"
636
+ " WHERE tag.tagname GLOB 'wiki-*'"
637
+ " AND tagxref.tagid=tag.tagid"
638
+ " GROUP BY 1"
639
+ ")"
640
+ "INSERT INTO x(label,url,score,date,snip)"
641
+ " SELECT printf('Wiki: %%s',name),"
642
+ " printf('%R/wiki?name=%%s',urlencode(name)),"
643
+ " search_score(),"
644
+ " datetime(mtime),"
645
+ " search_snippet()"
646
+ " FROM wiki"
647
+ " WHERE search_match(stext('w',rid,name));"
648
+ );
649
+ }
650
+ if( (srchFlags & SRCH_CKIN)!=0 ){
651
+ db_multi_exec(
652
+ "WITH ckin(uuid,rid,mtime) AS ("
653
+ " SELECT blob.uuid, event.objid, event.mtime"
654
+ " FROM event, blob"
655
+ " WHERE event.type='ci'"
656
+ " AND blob.rid=event.objid"
657
+ ")"
658
+ "INSERT INTO x(label,url,score,date,snip)"
659
+ " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime)),"
660
+ " printf('%R/timeline?c=%%s&n=8&y=ci',uuid),"
661
+ " search_score(),"
662
+ " datetime(mtime),"
663
+ " search_snippet()"
664
+ " FROM ckin"
665
+ " WHERE search_match(stext('c',rid,NULL));"
666
+ );
667
+ }
668
+ if( (srchFlags & SRCH_TKT)!=0 ){
669
+ db_multi_exec(
670
+ "INSERT INTO x(label,url,score, date,snip)"
671
+ " SELECT printf('Ticket [%%.17s] on %%s',"
672
+ "tkt_uuid,datetime(tkt_mtime)),"
673
+ " printf('%R/tktview/%%.20s',tkt_uuid),"
674
+ " search_score(),"
675
+ " datetime(tkt_mtime),"
676
+ " search_snippet()"
677
+ " FROM ticket"
678
+ " WHERE search_match(stext('t',tkt_id,NULL));"
679
+ );
680
+ }
681
+}
682
+
683
+/*
684
+** Implemenation of the rank() function used with rank(matchinfo(*,'pcsx')).
685
+*/
686
+static void search_rank_sqlfunc(
687
+ sqlite3_context *context,
688
+ int argc,
689
+ sqlite3_value **argv
690
+){
691
+ const unsigned *aVal = (unsigned int*)sqlite3_value_blob(argv[0]);
692
+ int nVal = sqlite3_value_bytes(argv[0])/4;
693
+ int nTerm; /* Number of search terms in the query */
694
+ int i; /* Loop counter */
695
+ double r = 1.0; /* Score */
696
+
697
+ if( nVal<6 ) return;
698
+ if( aVal[1]!=1 ) return;
699
+ nTerm = aVal[0];
700
+ r *= 1<<((30*(aVal[2]-1))/nTerm);
701
+ for(i=1; i<=nTerm; i++){
702
+ int hits_this_row = aVal[3*i];
703
+ int hits_all_rows = aVal[3*i+1];
704
+ int rows_with_hit = aVal[3*i+2];
705
+ double avg_hits_per_row = (double)hits_all_rows/(double)rows_with_hit;
706
+ r *= hits_this_row/avg_hits_per_row;
707
+ }
708
+#define SEARCH_DEBUG_RANK 0
709
+#if SEARCH_DEBUG_RANK
710
+ {
711
+ Blob x;
712
+ blob_init(&x,0,0);
713
+ blob_appendf(&x,"%08x", (int)r);
714
+ for(i=0; i<nVal; i++){
715
+ blob_appendf(&x," %d", aVal[i]);
716
+ }
717
+ blob_appendf(&x," r=%g", r);
718
+ sqlite3_result_text(context, blob_str(&x), -1, fossil_free);
719
+ }
720
+#else
721
+ sqlite3_result_double(context, r);
722
+#endif
723
+}
724
+
725
+/*
726
+** When this routine is called, there already exists a table
727
+**
728
+** x(label,url,score,date,snip).
729
+**
730
+** And the srchFlags parameter has been validated. This routine
731
+** fills the X table with search results using a index scan.
732
+**
733
+** The companion full-text scan routine is search_fullscan().
734
+*/
735
+static void search_indexed(
736
+ const char *zPattern, /* The query pattern */
737
+ unsigned int srchFlags /* What to search over */
738
+){
739
+ Blob sql;
740
+ if( srchFlags==0 ) return;
741
+ sqlite3_create_function(g.db, "rank", 1, SQLITE_UTF8, 0,
742
+ search_rank_sqlfunc, 0, 0);
743
+ blob_init(&sql, 0, 0);
744
+ blob_appendf(&sql,
745
+ "INSERT INTO x(label,url,score,date,snip) "
746
+ " SELECT ftsdocs.label,"
747
+ " ftsdocs.url,"
748
+ " rank(matchinfo(ftsidx,'pcsx')),"
749
+ " datetime(ftsdocs.mtime),"
750
+ " snippet(ftsidx,'<mark>','</mark>')"
751
+ " FROM ftsidx, ftsdocs"
752
+ " WHERE ftsidx MATCH %Q"
753
+ " AND ftsdocs.rowid=ftsidx.docid",
754
+ zPattern
755
+ );
756
+ if( srchFlags!=SRCH_ALL ){
757
+ const char *zSep = " AND (";
758
+ static const struct { unsigned m; char c; } aMask[] = {
759
+ { SRCH_CKIN, 'c' },
760
+ { SRCH_DOC, 'd' },
761
+ { SRCH_TKT, 't' },
762
+ { SRCH_WIKI, 'w' },
763
+ };
764
+ int i;
765
+ for(i=0; i<ArraySize(aMask); i++){
766
+ if( srchFlags & aMask[i].m ){
767
+ blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c);
768
+ zSep = " OR ";
769
+ }
770
+ }
771
+ blob_append(&sql,")",1);
772
+ }
773
+ db_multi_exec("%s",blob_str(&sql)/*safe-for-%s*/);
774
+#if SEARCH_DEBUG_RANK
775
+ db_multi_exec("UPDATE x SET label=printf('%%s (score=%%s)',label,score)");
776
+#endif
777
+}
778
+
574779
575780
/*
576781
** This routine generates web-page output for a search operation.
577782
** Other web-pages can invoke this routine to add search results
578783
** in the middle of the page.
@@ -588,91 +793,32 @@
588793
589794
srchFlags = search_restrict(srchFlags);
590795
if( srchFlags==0 ) return 0;
591796
search_sql_setup(g.db);
592797
add_content_sql_commands(g.db);
593
- search_init(zPattern, "<b>", "</b>", " ... ",
594
- SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE);
595
- db_multi_exec(
596
- "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
597
- "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);"
598
- );
599
- if( (srchFlags & SRCH_DOC)!=0 ){
600
- char *zDocGlob = db_get("doc-glob","");
601
- char *zDocBr = db_get("doc-branch","trunk");
602
- if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){
603
- db_multi_exec(
604
- "INSERT INTO x(label,url,date,snip)"
605
- " SELECT printf('Document: %%s',foci.filename),"
606
- " printf('%R/doc/%T/%%s',foci.filename),"
607
- " (SELECT datetime(event.mtime) FROM event"
608
- " WHERE objid=symbolic_name_to_rid('trunk')),"
609
- " snippet(stext('d',blob.rid,foci.filename))"
610
- " FROM foci CROSS JOIN blob"
611
- " WHERE checkinID=symbolic_name_to_rid('trunk')"
612
- " AND blob.uuid=foci.uuid"
613
- " AND %z",
614
- zDocBr, glob_expr("foci.filename", zDocGlob)
615
- );
616
- }
617
- }
618
- if( (srchFlags & SRCH_WIKI)!=0 ){
619
- db_multi_exec(
620
- "WITH wiki(name,rid,mtime) AS ("
621
- " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)"
622
- " FROM tag, tagxref"
623
- " WHERE tag.tagname GLOB 'wiki-*'"
624
- " AND tagxref.tagid=tag.tagid"
625
- " GROUP BY 1"
626
- ")"
627
- "INSERT INTO x(label,url,date,snip)"
628
- " SELECT printf('Wiki: %%s',name),"
629
- " printf('%R/wiki?name=%%s',urlencode(name)),"
630
- " datetime(mtime),"
631
- " snippet(stext('w',rid,name))"
632
- " FROM wiki;"
633
- );
634
- }
635
- if( (srchFlags & SRCH_CKIN)!=0 ){
636
- db_multi_exec(
637
- "WITH ckin(uuid,rid,mtime) AS ("
638
- " SELECT blob.uuid, event.objid, event.mtime"
639
- " FROM event, blob"
640
- " WHERE event.type='ci'"
641
- " AND blob.rid=event.objid"
642
- ")"
643
- "INSERT INTO x(label,url,date,snip)"
644
- " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime)),"
645
- " printf('%R/timeline?c=%%s&n=8&y=ci',uuid),"
646
- " datetime(mtime),"
647
- " snippet(stext('c',rid,NULL))"
648
- " FROM ckin;"
649
- );
650
- }
651
- if( (srchFlags & SRCH_TKT)!=0 ){
652
- db_multi_exec(
653
- "INSERT INTO x(label,url,date,snip)"
654
- " SELECT printf('Ticket [%%.17s] on %%s',"
655
- "tkt_uuid,datetime(tkt_mtime)),"
656
- " printf('%R/tktview/%%.20s',tkt_uuid),"
657
- " datetime(tkt_mtime),"
658
- " snippet(stext('t',tkt_id,NULL))"
659
- " FROM ticket;"
660
- );
661
- }
662
- db_prepare(&q, "SELECT url, substr(snip,9), label"
663
- " FROM x WHERE snip IS NOT NULL"
664
- " ORDER BY substr(snip,1,8) DESC, date DESC;");
798
+ db_multi_exec(
799
+ "CREATE TEMP TABLE x(label,url,score,date,snip);"
800
+ );
801
+ if( !search_index_exists() ){
802
+ search_fullscan(zPattern, srchFlags);
803
+ }else{
804
+ search_update_index(srchFlags);
805
+ search_indexed(zPattern, srchFlags);
806
+ }
807
+ db_prepare(&q, "SELECT url, snip, label"
808
+ " FROM x"
809
+ " ORDER BY score DESC, date DESC;");
665810
while( db_step(&q)==SQLITE_ROW ){
666811
const char *zUrl = db_column_text(&q, 0);
667812
const char *zSnippet = db_column_text(&q, 1);
668813
const char *zLabel = db_column_text(&q, 2);
669814
if( nRow==0 ){
670815
@ <ol>
671816
}
672817
nRow++;
673
- @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br>%s(zSnippet)</li>
818
+ @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br>
819
+ @ <span class='snippet'>%s(zSnippet)</span></li>
674820
}
675821
db_finalize(&q);
676822
if( nRow ){
677823
@ </ol>
678824
}
@@ -789,18 +935,18 @@
789935
** zName Name of the object being searched.
790936
*/
791937
void search_stext(
792938
char cType, /* Type of document */
793939
int rid, /* BLOB.RID or TAG.TAGID value for document */
794
- const char *zName, /* Name of the document */
940
+ const char *zName, /* Auxiliary information */
795941
Blob *pOut /* OUT: Initialize to the search text */
796942
){
797943
blob_init(pOut, 0, 0);
798944
switch( cType ){
799945
case 'd': { /* Documents */
800946
Blob doc;
801
- content_get(rid, &doc);
947
+ content_get(rid, &doc);
802948
blob_to_utf8_no_bom(&doc, 0);
803949
get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
804950
blob_reset(&doc);
805951
break;
806952
}
@@ -860,63 +1006,355 @@
8601006
break;
8611007
}
8621008
}
8631009
}
8641010
865
-/*
866
-** The arguments cType,rid,zName define an object that can be searched
867
-** for. Return a URL (relative to the root of the Fossil project) that
868
-** will jump to that document.
869
-**
870
-** Space to hold the returned string is obtained from mprintf() and should
871
-** be freed by the caller using fossil_free() or the equivalent.
872
-*/
873
-char *search_url(
874
- char cType, /* Type of document */
875
- int rid, /* BLOB.RID or TAG.TAGID for the object */
876
- const char *zName /* Name of the object */
877
-){
878
- char *zUrl = 0;
879
- switch( cType ){
880
- case 'd': { /* Documents */
881
- zUrl = db_text(0,
882
- "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)"
883
- " FROM mlink, blob"
884
- " WHERE mlink.fid=%d AND mlink.mid=blob.rid",
885
- zName, rid);
886
- break;
887
- }
888
- case 'w': { /* Wiki */
889
- char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
890
- zUrl = mprintf("/wiki?id=%z&name=%t", zId, zName);
891
- break;
892
- }
893
- case 'c': { /* Ckeck-in Comment */
894
- char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
895
- zUrl = mprintf("/info/%z", zId);
896
- break;
897
- }
898
- case 't': { /* Tickets */
899
- char *zId = db_text(0, "SELECT tkt_uuid FROM ticket"
900
- " WHERE tkt_id=%d", rid);
901
- zUrl = mprintf("/tktview/%.20z", zId);
902
- break;
903
- }
904
- }
905
- return zUrl;
906
-}
907
-
9081011
/*
9091012
** COMMAND: test-search-stext
9101013
**
9111014
** Usage: fossil test-search-stext TYPE ARG1 ARG2
9121015
*/
9131016
void test_search_stext(void){
9141017
Blob out;
915
- char *zUrl;
9161018
db_find_and_open_repository(0,0);
9171019
if( g.argc!=5 ) usage("TYPE RID NAME");
9181020
search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out);
919
- zUrl = search_url(g.argv[2][0], atoi(g.argv[3]), g.argv[4]);
920
- fossil_print("%s\n%z\n",blob_str(&out),zUrl);
1021
+ fossil_print("%s\n",blob_str(&out));
9211022
blob_reset(&out);
9221023
}
1024
+
1025
+/* The schema for the full-text index
1026
+*/
1027
+static const char zFtsSchema[] =
1028
+@ -- One entry for each possible search result
1029
+@ CREATE TABLE IF NOT EXISTS "%w".ftsdocs(
1030
+@ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid
1031
+@ type CHAR(1), -- Type of document
1032
+@ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document
1033
+@ name TEXT, -- Additional document description
1034
+@ idxed BOOLEAN, -- True if currently in the index
1035
+@ label TEXT, -- Label to print on search results
1036
+@ url TEXT, -- URL to access this document
1037
+@ mtime DATE, -- Date when document created
1038
+@ UNIQUE(type,rid)
1039
+@ );
1040
+@ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0;
1041
+@ CREATE INDEX "%w".ftsdocName ON ftsdocs(name) WHERE type='w';
1042
+@ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS
1043
+@ SELECT rowid, type, rid, name, idxed, label, url, mtime,
1044
+@ stext(type,rid,name) AS 'stext'
1045
+@ FROM ftsdocs;
1046
+@ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx
1047
+@ USING fts4(content="ftscontent", stext);
1048
+;
1049
+static const char zFtsDrop[] =
1050
+@ DROP TABLE IF EXISTS "%w".ftsidx;
1051
+@ DROP VIEW IF EXISTS "%w".ftscontent;
1052
+@ DROP TABLE IF EXISTS "%w".ftsdocs;
1053
+;
1054
+
1055
+/*
1056
+** Create or drop the tables associated with a full-text index.
1057
+*/
1058
+void search_create_index(void){
1059
+ const char *zDb = db_name("repository");
1060
+ search_sql_setup(g.db);
1061
+ db_multi_exec(zFtsSchema/*works-like:"%w%w%w%w%w"*/,
1062
+ zDb, zDb, zDb, zDb, zDb);
1063
+}
1064
+void search_drop_index(void){
1065
+ const char *zDb = db_name("repository");
1066
+ db_multi_exec(zFtsDrop/*works-like:"%w%w%w"*/, zDb, zDb, zDb);
1067
+}
1068
+
1069
+/*
1070
+** Return true if the full-text search index exists
1071
+*/
1072
+int search_index_exists(void){
1073
+ static int fExists = -1;
1074
+ if( fExists<0 ) fExists = db_table_exists("repository","ftsdocs");
1075
+ return fExists;
1076
+}
1077
+
1078
+/*
1079
+** Fill the FTSDOCS table with unindexed entries for everything
1080
+** in the repository. This uses INSERT OR IGNORE so entries already
1081
+** in FTSDOCS are unchanged.
1082
+*/
1083
+void search_fill_index(void){
1084
+ if( !search_index_exists() ) return;
1085
+ search_sql_setup(g.db);
1086
+ db_multi_exec(
1087
+ "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
1088
+ " SELECT 'c', objid, 0 FROM event WHERE type='ci';"
1089
+ );
1090
+ db_multi_exec(
1091
+ "WITH latest_wiki(rid,name,mtime) AS ("
1092
+ " SELECT tagxref.rid, substr(tag.tagname,6), max(tagxref.mtime)"
1093
+ " FROM tag, tagxref"
1094
+ " WHERE tag.tagname GLOB 'wiki-*'"
1095
+ " AND tagxref.tagid=tag.tagid"
1096
+ " AND tagxref.value>0"
1097
+ " GROUP BY 2"
1098
+ ") INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)"
1099
+ " SELECT 'w', rid, name, 0 FROM latest_wiki;"
1100
+ );
1101
+ db_multi_exec(
1102
+ "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
1103
+ " SELECT 't', tkt_id, 0 FROM ticket;"
1104
+ );
1105
+}
1106
+
1107
+/*
1108
+** The document described by cType,rid,zName is about to be added or
1109
+** updated. If the document has already been indexed, then unindex it
1110
+** now while we still have access to the old content. Add the document
1111
+** to the queue of documents that need to be indexed or reindexed.
1112
+*/
1113
+void search_doc_touch(char cType, int rid, const char *zName){
1114
+ if( search_index_exists() ){
1115
+ char zType[2];
1116
+ zType[0] = cType;
1117
+ zType[1] = 0;
1118
+ db_multi_exec(
1119
+ "DELETE FROM ftsidx WHERE docid IN"
1120
+ " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)",
1121
+ zType, rid
1122
+ );
1123
+ db_multi_exec(
1124
+ "REPLACE INTO ftsdocs(type,rid,name,idxed)"
1125
+ " VALUES(%Q,%d,%Q,0)",
1126
+ zType, rid, zName
1127
+ );
1128
+ if( cType=='w' ){
1129
+ db_multi_exec(
1130
+ "DELETE FROM ftsidx WHERE docid IN"
1131
+ " (SELECT rowid FROM ftsdocs WHERE type='w' AND name=%Q AND idxed)",
1132
+ zName
1133
+ );
1134
+ db_multi_exec(
1135
+ "DELETE FROM ftsdocs WHERE type='w' AND name=%Q AND rid!=%d",
1136
+ zName, rid
1137
+ );
1138
+ }
1139
+ }
1140
+}
1141
+
1142
+/*
1143
+** If the doc-glob and doc-br settings are valid for document search
1144
+** and if the latest check-in on doc-br is in the unindexed set of
1145
+** check-ins, then update all 'd' entries in FTSDOCS that have
1146
+** changed.
1147
+*/
1148
+static void search_update_doc_index(void){
1149
+ const char *zDocBr = db_get("doc-branch","trunk");
1150
+ int ckid = zDocBr ? symbolic_name_to_rid(zDocBr,"ci") : 0;
1151
+ double rTime;
1152
+ char *zBrUuid;
1153
+ if( ckid==0 ) return;
1154
+ if( !db_exists("SELECT 1 FROM ftsdocs WHERE type='c' AND rid=%d"
1155
+ " AND NOT idxed", ckid) ) return;
1156
+
1157
+ /* If we get this far, it means that changes to 'd' entries are
1158
+ ** required. */
1159
+ rTime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", ckid);
1160
+ zBrUuid = db_text("","SELECT substr(uuid,1,20) FROM blob WHERE rid=%d",ckid);
1161
+ db_multi_exec(
1162
+ "CREATE TEMP TABLE current_docs(rid INTEGER PRIMARY KEY, name);"
1163
+ "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
1164
+ "INSERT OR IGNORE INTO current_docs(rid, name)"
1165
+ " SELECT blob.rid, foci.filename FROM foci, blob"
1166
+ " WHERE foci.checkinID=%d AND blob.uuid=foci.uuid"
1167
+ " AND %z",
1168
+ ckid, glob_expr("foci.filename", db_get("doc-glob",""))
1169
+ );
1170
+ db_multi_exec(
1171
+ "DELETE FROM ftsidx WHERE docid IN"
1172
+ " (SELECT rowid FROM ftsdocs WHERE type='d'"
1173
+ " AND rid NOT IN (SELECT rid FROM current_docs))"
1174
+ );
1175
+ db_multi_exec(
1176
+ "DELETE FROM ftsdocs WHERE type='d'"
1177
+ " AND rid NOT IN (SELECT rid FROM current_docs)"
1178
+ );
1179
+ db_multi_exec(
1180
+ "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed,label,url,mtime)"
1181
+ " SELECT 'd', rid, name, 0,"
1182
+ " printf('Document: %%s',name),"
1183
+ " printf('/doc/%q/%%s',urlencode(name)),"
1184
+ " %.17g"
1185
+ " FROM current_docs",
1186
+ zBrUuid, rTime
1187
+ );
1188
+ db_multi_exec(
1189
+ "INSERT INTO ftsidx(docid,stext)"
1190
+ " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed"
1191
+ );
1192
+ db_multi_exec(
1193
+ "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed"
1194
+ );
1195
+}
1196
+
1197
+/*
1198
+** Deal with all of the unindexed 'c' terms in FTSDOCS
1199
+*/
1200
+static void search_update_checkin_index(void){
1201
+ db_multi_exec(
1202
+ "INSERT INTO ftsidx(docid,stext)"
1203
+ " SELECT rowid, stext('c',rid,NULL) FROM ftsdocs"
1204
+ " WHERE type='c' AND NOT idxed;"
1205
+ );
1206
+ db_multi_exec(
1207
+ "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1208
+ " SELECT ftsdocs.rowid, 1, 'c', ftsdocs.rid, NULL,"
1209
+ " printf('Check-in [%%.16s] on %%s',blob.uuid,datetime(event.mtime)),"
1210
+ " printf('/timeline?y=ci&n=9&c=%%.20s',blob.uuid),"
1211
+ " event.mtime"
1212
+ " FROM ftsdocs, event, blob"
1213
+ " WHERE ftsdocs.type='c' AND NOT ftsdocs.idxed"
1214
+ " AND event.objid=ftsdocs.rid"
1215
+ " AND blob.rid=ftsdocs.rid"
1216
+ );
1217
+}
1218
+
1219
+/*
1220
+** Deal with all of the unindexed 't' terms in FTSDOCS
1221
+*/
1222
+static void search_update_ticket_index(void){
1223
+ db_multi_exec(
1224
+ "INSERT INTO ftsidx(docid,stext)"
1225
+ " SELECT rowid, stext('t',rid,NULL) FROM ftsdocs"
1226
+ " WHERE type='t' AND NOT idxed;"
1227
+ );
1228
+ if( db_changes()==0 ) return;
1229
+ db_multi_exec(
1230
+ "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1231
+ " SELECT ftsdocs.rowid, 1, 't', ftsdocs.rid, NULL,"
1232
+ " printf('Ticket [%%.16s] on %%s',tkt_uuid,datetime(tkt_mtime)),"
1233
+ " printf('/tktview/%%.20s',tkt_uuid),"
1234
+ " tkt_mtime"
1235
+ " FROM ftsdocs, ticket"
1236
+ " WHERE ftsdocs.type='t' AND NOT ftsdocs.idxed"
1237
+ " AND ticket.tkt_id=ftsdocs.rid"
1238
+ );
1239
+}
1240
+
1241
+/*
1242
+** Deal with all of the unindexed 'w' terms in FTSDOCS
1243
+*/
1244
+static void search_update_wiki_index(void){
1245
+ db_multi_exec(
1246
+ "INSERT INTO ftsidx(docid,stext)"
1247
+ " SELECT rowid, stext('w',rid,NULL) FROM ftsdocs"
1248
+ " WHERE type='w' AND NOT idxed;"
1249
+ );
1250
+ if( db_changes()==0 ) return;
1251
+ db_multi_exec(
1252
+ "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1253
+ " SELECT ftsdocs.rowid, 1, 'w', ftsdocs.rid, ftsdocs.name,"
1254
+ " 'Wiki: '||ftsdocs.name,"
1255
+ " '/wiki?name='||urlencode(ftsdocs.name),"
1256
+ " tagxref.mtime"
1257
+ " FROM ftsdocs, tagxref"
1258
+ " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed"
1259
+ " AND tagxref.rid=ftsdocs.rid"
1260
+ );
1261
+}
1262
+
1263
+/*
1264
+** Deal with all of the unindexed entries in the FTSDOCS table - that
1265
+** is to say, all the entries with FTSDOCS.IDXED=0. Add them to the
1266
+** index.
1267
+*/
1268
+void search_update_index(unsigned int srchFlags){
1269
+ if( !search_index_exists() ) return;
1270
+ if( !db_exists("SELECT 1 FROM ftsdocs WHERE NOT idxed") ) return;
1271
+ search_sql_setup(g.db);
1272
+ if( srchFlags & (SRCH_CKIN|SRCH_DOC) ){
1273
+ search_update_doc_index();
1274
+ search_update_checkin_index();
1275
+ }
1276
+ if( srchFlags & SRCH_TKT ){
1277
+ search_update_ticket_index();
1278
+ }
1279
+ if( srchFlags & SRCH_WIKI ){
1280
+ search_update_wiki_index();
1281
+ }
1282
+}
1283
+
1284
+/*
1285
+** COMMAND: test-fts
1286
+*/
1287
+void test_fts_cmd(void){
1288
+ char *zSubCmd;
1289
+ int i, n;
1290
+ static const struct { int iCmd; const char *z; } aCmd[] = {
1291
+ { 1, "create" },
1292
+ { 2, "drop" },
1293
+ { 3, "exists" },
1294
+ { 4, "fill" },
1295
+ { 8, "refill" },
1296
+ { 5, "pending" },
1297
+ { 7, "update" },
1298
+ };
1299
+ db_find_and_open_repository(0, 0);
1300
+ if( g.argc<3 ) usage("SUBCMD ...");
1301
+ zSubCmd = g.argv[2];
1302
+ n = (int)strlen(zSubCmd);
1303
+ for(i=0; i<ArraySize(aCmd); i++){
1304
+ if( fossil_strncmp(aCmd[i].z, zSubCmd, n)==0 ) break;
1305
+ }
1306
+ if( i>=ArraySize(aCmd) ){
1307
+ Blob all;
1308
+ blob_init(&all,0,0);
1309
+ for(i=0; i<ArraySize(aCmd); i++) blob_appendf(&all, " %s", aCmd[i].z);
1310
+ fossil_fatal("unknown \"%s\" - should be:%s", zSubCmd, blob_str(&all));
1311
+ return;
1312
+ }
1313
+ db_begin_transaction();
1314
+ switch( aCmd[i].iCmd ){
1315
+ case 1: { assert( fossil_strncmp(zSubCmd, "create", n)==0 );
1316
+ search_create_index();
1317
+ break;
1318
+ }
1319
+ case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 );
1320
+ search_drop_index();
1321
+ break;
1322
+ }
1323
+ case 3: { assert( fossil_strncmp(zSubCmd, "exists", n)==0 );
1324
+ fossil_print("search_index_exists() = %d\n", search_index_exists());
1325
+ break;
1326
+ }
1327
+ case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 );
1328
+ search_fill_index();
1329
+ break;
1330
+ }
1331
+ case 8: { assert( fossil_strncmp(zSubCmd, "refill", n)==0 );
1332
+ search_drop_index();
1333
+ search_create_index();
1334
+ search_fill_index();
1335
+ break;
1336
+ }
1337
+ case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 );
1338
+ Stmt q;
1339
+ if( !search_index_exists() ) break;
1340
+ db_prepare(&q, "SELECT rowid,type,rid,quote(name) FROM ftsdocs"
1341
+ " WHERE NOT idxed");
1342
+ while( db_step(&q)==SQLITE_ROW ){
1343
+ fossil_print("%6d: %s %6d %s\n",
1344
+ db_column_int(&q, 0),
1345
+ db_column_text(&q, 1),
1346
+ db_column_int(&q, 2),
1347
+ db_column_text(&q, 3)
1348
+ );
1349
+ }
1350
+ db_finalize(&q);
1351
+ break;
1352
+ }
1353
+ case 7: { assert( fossil_strncmp(zSubCmd, "update", n)==0 );
1354
+ search_update_index(SRCH_ALL);
1355
+ break;
1356
+ }
1357
+
1358
+ }
1359
+ db_end_transaction(0);
1360
+}
9231361
--- src/search.c
+++ src/search.c
@@ -45,14 +45,15 @@
45 char *zPattern; /* The search pattern */
46 char *zMarkBegin; /* Start of a match */
47 char *zMarkEnd; /* End of a match */
48 char *zMarkGap; /* A gap between two matches */
49 unsigned fSrchFlg; /* Flags */
 
 
50 };
51
52 #define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */
53 #define SRCHFLG_SCORE 0x02 /* Prepend the score to each snippet */
54 #define SRCHFLG_STATIC 0x04 /* The static gSearch object */
55
56 #endif
57
58 /*
@@ -92,10 +93,11 @@
92 if( p ){
93 fossil_free(p->zPattern);
94 fossil_free(p->zMarkBegin);
95 fossil_free(p->zMarkEnd);
96 fossil_free(p->zMarkGap);
 
97 memset(p, 0, sizeof(*p));
98 if( p!=&gSearch ) fossil_free(p);
99 }
100 }
101
@@ -123,10 +125,11 @@
123 p->zPattern = z = mprintf("%s", zPattern);
124 p->zMarkBegin = mprintf("%s", zMarkBegin);
125 p->zMarkEnd = mprintf("%s", zMarkEnd);
126 p->zMarkGap = mprintf("%s", zMarkGap);
127 p->fSrchFlg = fSrchFlg;
 
128 while( *z && p->nTerm<SEARCH_MAX_TERM ){
129 while( *z && !ISALNUM(*z) ){ z++; }
130 if( *z==0 ) break;
131 p->a[p->nTerm].z = z;
132 for(i=1; ISALNUM(z[i]); i++){}
@@ -156,24 +159,26 @@
156 }
157 }
158
159 /*
160 ** Compare a search pattern against one or more input strings which
161 ** collectively comprise a document. Return a match score. Optionally
162 ** also return a "snippet".
 
 
 
163 **
164 ** Scoring:
165 ** * All terms must match at least once or the score is zero
166 ** * One point for each matching term
167 ** * Extra points if consecutive words of the pattern are consecutive
168 ** in the document
169 */
170 static int search_score(
171 Search *p, /* Search pattern and flags */
172 int nDoc, /* Number of strings in this document */
173 const char **azDoc, /* Text of each string */
174 Blob *pSnip /* If not NULL: Write a snippet here */
175 ){
176 int score; /* Final score */
177 int i; /* Offset into current document */
178 int ii; /* Loop counter */
179 int j; /* Loop over search terms */
@@ -226,18 +231,17 @@
226 /* Finished search all documents.
227 ** Every term must be seen or else the score is zero
228 */
229 score = 1;
230 for(j=0; j<p->nTerm; j++) score *= anMatch[j];
231 if( score==0 || pSnip==0 ) return score;
 
 
232
233
234 /* Prepare a snippet that describes the matching text.
235 */
236 blob_init(pSnip, 0, 0);
237 if( p->fSrchFlg & SRCHFLG_SCORE ) blob_appendf(pSnip, "%08x", score);
238
239 while(1){
240 int iOfst;
241 int iTail;
242 int iBest;
243 for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){}
@@ -272,11 +276,11 @@
272 if( iOfst<0 ) iOfst = 0;
273 while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--;
274 while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++;
275 for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){}
276 while( ISALNUM(zDoc[iTail]) ) iTail++;
277 if( iOfst>0 || wantGap ) blob_append(pSnip, p->zMarkGap, -1);
278 wantGap = zDoc[iTail]!=0;
279 zDoc += iOfst;
280 iTail -= iOfst;
281
282 /* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */
@@ -285,53 +289,51 @@
285 for(j=0; j<p->nTerm; j++){
286 int n = p->a[j].n;
287 if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0
288 && (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*')
289 ){
290 snippet_text_append(p, pSnip, zDoc, i);
291 zDoc += i;
292 iTail -= i;
293 blob_append(pSnip, p->zMarkBegin, -1);
294 if( p->a[j].z[n]=='*' ){
295 while( ISALNUM(zDoc[n]) ) n++;
296 }
297 snippet_text_append(p, pSnip, zDoc, n);
298 zDoc += n;
299 iTail -= n;
300 blob_append(pSnip, p->zMarkEnd, -1);
301 i = -1;
302 break;
303 } /* end-if */
304 } /* end for(j) */
305 if( j<p->nTerm ){
306 while( ISALNUM(zDoc[i]) && i<iTail ){ i++; }
307 }
308 } /* end for(i) */
309 snippet_text_append(p, pSnip, zDoc, iTail);
310 }
311 if( wantGap ) blob_append(pSnip, p->zMarkGap, -1);
312 return score;
313 }
314
315 /*
316 ** COMMAND: test-snippet
317 **
318 ** Usage: fossil test-snippet SEARCHSTRING FILE1 FILE2 ...
319 */
320 void test_snippet_cmd(void){
321 Search *p;
322 int i;
323 Blob x;
324 Blob snip;
325 int score;
326 char *zDoc;
327 int flg = 0;
328 char *zBegin = (char*)find_option("begin",0,1);
329 char *zEnd = (char*)find_option("end",0,1);
330 char *zGap = (char*)find_option("gap",0,1);
331 if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML;
332 if( find_option("score",0,0)!=0 ) flg |= SRCHFLG_SCORE;
333 if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC;
334 verify_all_options();
335 if( g.argc<4 ) usage("SEARCHSTRING FILE1...");
336 if( zBegin==0 ) zBegin = "[[";
337 if( zEnd==0 ) zEnd = "]]";
@@ -338,18 +340,18 @@
338 if( zGap==0 ) zGap = " ... ";
339 p = search_init(g.argv[2], zBegin, zEnd, zGap, flg);
340 for(i=3; i<g.argc; i++){
341 blob_read_from_file(&x, g.argv[i]);
342 zDoc = blob_str(&x);
343 score = search_score(p, 1, (const char**)&zDoc, &snip);
344 fossil_print("%s: %d\n", g.argv[i], score);
345 blob_reset(&x);
346 if( score ){
347 fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&snip), '=');
348 blob_reset(&snip);
349 }
350 }
 
351 }
352
353 /*
354 ** An SQL function to initialize the global search pattern:
355 **
@@ -361,12 +363,12 @@
361 sqlite3_context *context,
362 int argc,
363 sqlite3_value **argv
364 ){
365 const char *zPattern = 0;
366 const char *zBegin = "<b>";
367 const char *zEnd = "</b>";
368 const char *zGap = " ... ";
369 unsigned int flg = SRCHFLG_HTML;
370 switch( argc ){
371 default:
372 flg = (unsigned int)sqlite3_value_int(argv[4]);
@@ -385,35 +387,45 @@
385 search_end(&gSearch);
386 }
387 }
388
389 /*
390 ** This is an SQLite function that scores its input using
391 ** the pattern from the previous call to search_init().
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392 */
393 static void search_score_sqlfunc(
394 sqlite3_context *context,
395 int argc,
396 sqlite3_value **argv
397 ){
398 int isSnippet = sqlite3_user_data(context)!=0;
399 const char **azDoc;
400 int score;
401 int i;
402 Blob snip;
403
404 if( gSearch.nTerm==0 ) return;
405 azDoc = fossil_malloc( sizeof(const char*)*(argc+1) );
406 for(i=0; i<argc; i++) azDoc[i] = (const char*)sqlite3_value_text(argv[i]);
407 score = search_score(&gSearch, argc, azDoc, isSnippet ? &snip : 0);
408 fossil_free((void *)azDoc);
409 if( isSnippet ){
410 if( score ){
411 sqlite3_result_text(context, blob_materialize(&snip), -1, fossil_free);
412 }
413 }else{
414 sqlite3_result_int(context, score);
415 }
416 }
417
418 /*
419 ** This is an SQLite function that computes the searchable text.
@@ -449,14 +461,18 @@
449 ** Register the "score()" SQL function to score its input text
450 ** using the given Search object. Once this function is registered,
451 ** do not delete the Search object.
452 */
453 void search_sql_setup(sqlite3 *db){
454 sqlite3_create_function(db, "score", -1, SQLITE_UTF8, 0,
455 search_score_sqlfunc, 0, 0);
456 sqlite3_create_function(db, "snippet", -1, SQLITE_UTF8, &gSearch,
 
 
457 search_score_sqlfunc, 0, 0);
 
 
458 sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0,
459 search_init_sqlfunc, 0, 0);
460 sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0,
461 search_stext_sqlfunc, 0, 0);
462 sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0,
@@ -551,28 +567,217 @@
551 /*
552 ** Remove bits from srchFlags which are disallowed by either the
553 ** current server configuration or by user permissions.
554 */
555 unsigned int search_restrict(unsigned int srchFlags){
556 if( (srchFlags & SRCH_CKIN)!=0
557 && (g.perm.Read==0 || db_get_boolean("search-ci",0)==0) ){
 
 
 
558 srchFlags &= ~SRCH_CKIN;
559 }
560 if( (srchFlags & SRCH_DOC)!=0
561 && (g.perm.Read==0 || db_get_boolean("search-doc",0)==0) ){
562 srchFlags &= ~SRCH_DOC;
563 }
564 if( (srchFlags & SRCH_TKT)!=0
565 && (g.perm.RdTkt==0 || db_get_boolean("search-tkt",0)==0) ){
566 srchFlags &= ~SRCH_TKT;
567 }
568 if( (srchFlags & SRCH_WIKI)!=0
569 && (g.perm.RdWiki==0 || db_get_boolean("search-wiki",0)==0) ){
570 srchFlags &= ~SRCH_WIKI;
571 }
572 return srchFlags;
573 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
575 /*
576 ** This routine generates web-page output for a search operation.
577 ** Other web-pages can invoke this routine to add search results
578 ** in the middle of the page.
@@ -588,91 +793,32 @@
588
589 srchFlags = search_restrict(srchFlags);
590 if( srchFlags==0 ) return 0;
591 search_sql_setup(g.db);
592 add_content_sql_commands(g.db);
593 search_init(zPattern, "<b>", "</b>", " ... ",
594 SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE);
595 db_multi_exec(
596 "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
597 "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);"
598 );
599 if( (srchFlags & SRCH_DOC)!=0 ){
600 char *zDocGlob = db_get("doc-glob","");
601 char *zDocBr = db_get("doc-branch","trunk");
602 if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){
603 db_multi_exec(
604 "INSERT INTO x(label,url,date,snip)"
605 " SELECT printf('Document: %%s',foci.filename),"
606 " printf('%R/doc/%T/%%s',foci.filename),"
607 " (SELECT datetime(event.mtime) FROM event"
608 " WHERE objid=symbolic_name_to_rid('trunk')),"
609 " snippet(stext('d',blob.rid,foci.filename))"
610 " FROM foci CROSS JOIN blob"
611 " WHERE checkinID=symbolic_name_to_rid('trunk')"
612 " AND blob.uuid=foci.uuid"
613 " AND %z",
614 zDocBr, glob_expr("foci.filename", zDocGlob)
615 );
616 }
617 }
618 if( (srchFlags & SRCH_WIKI)!=0 ){
619 db_multi_exec(
620 "WITH wiki(name,rid,mtime) AS ("
621 " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)"
622 " FROM tag, tagxref"
623 " WHERE tag.tagname GLOB 'wiki-*'"
624 " AND tagxref.tagid=tag.tagid"
625 " GROUP BY 1"
626 ")"
627 "INSERT INTO x(label,url,date,snip)"
628 " SELECT printf('Wiki: %%s',name),"
629 " printf('%R/wiki?name=%%s',urlencode(name)),"
630 " datetime(mtime),"
631 " snippet(stext('w',rid,name))"
632 " FROM wiki;"
633 );
634 }
635 if( (srchFlags & SRCH_CKIN)!=0 ){
636 db_multi_exec(
637 "WITH ckin(uuid,rid,mtime) AS ("
638 " SELECT blob.uuid, event.objid, event.mtime"
639 " FROM event, blob"
640 " WHERE event.type='ci'"
641 " AND blob.rid=event.objid"
642 ")"
643 "INSERT INTO x(label,url,date,snip)"
644 " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime)),"
645 " printf('%R/timeline?c=%%s&n=8&y=ci',uuid),"
646 " datetime(mtime),"
647 " snippet(stext('c',rid,NULL))"
648 " FROM ckin;"
649 );
650 }
651 if( (srchFlags & SRCH_TKT)!=0 ){
652 db_multi_exec(
653 "INSERT INTO x(label,url,date,snip)"
654 " SELECT printf('Ticket [%%.17s] on %%s',"
655 "tkt_uuid,datetime(tkt_mtime)),"
656 " printf('%R/tktview/%%.20s',tkt_uuid),"
657 " datetime(tkt_mtime),"
658 " snippet(stext('t',tkt_id,NULL))"
659 " FROM ticket;"
660 );
661 }
662 db_prepare(&q, "SELECT url, substr(snip,9), label"
663 " FROM x WHERE snip IS NOT NULL"
664 " ORDER BY substr(snip,1,8) DESC, date DESC;");
665 while( db_step(&q)==SQLITE_ROW ){
666 const char *zUrl = db_column_text(&q, 0);
667 const char *zSnippet = db_column_text(&q, 1);
668 const char *zLabel = db_column_text(&q, 2);
669 if( nRow==0 ){
670 @ <ol>
671 }
672 nRow++;
673 @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br>%s(zSnippet)</li>
 
674 }
675 db_finalize(&q);
676 if( nRow ){
677 @ </ol>
678 }
@@ -789,18 +935,18 @@
789 ** zName Name of the object being searched.
790 */
791 void search_stext(
792 char cType, /* Type of document */
793 int rid, /* BLOB.RID or TAG.TAGID value for document */
794 const char *zName, /* Name of the document */
795 Blob *pOut /* OUT: Initialize to the search text */
796 ){
797 blob_init(pOut, 0, 0);
798 switch( cType ){
799 case 'd': { /* Documents */
800 Blob doc;
801 content_get(rid, &doc);
802 blob_to_utf8_no_bom(&doc, 0);
803 get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
804 blob_reset(&doc);
805 break;
806 }
@@ -860,63 +1006,355 @@
860 break;
861 }
862 }
863 }
864
865 /*
866 ** The arguments cType,rid,zName define an object that can be searched
867 ** for. Return a URL (relative to the root of the Fossil project) that
868 ** will jump to that document.
869 **
870 ** Space to hold the returned string is obtained from mprintf() and should
871 ** be freed by the caller using fossil_free() or the equivalent.
872 */
873 char *search_url(
874 char cType, /* Type of document */
875 int rid, /* BLOB.RID or TAG.TAGID for the object */
876 const char *zName /* Name of the object */
877 ){
878 char *zUrl = 0;
879 switch( cType ){
880 case 'd': { /* Documents */
881 zUrl = db_text(0,
882 "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)"
883 " FROM mlink, blob"
884 " WHERE mlink.fid=%d AND mlink.mid=blob.rid",
885 zName, rid);
886 break;
887 }
888 case 'w': { /* Wiki */
889 char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
890 zUrl = mprintf("/wiki?id=%z&name=%t", zId, zName);
891 break;
892 }
893 case 'c': { /* Ckeck-in Comment */
894 char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
895 zUrl = mprintf("/info/%z", zId);
896 break;
897 }
898 case 't': { /* Tickets */
899 char *zId = db_text(0, "SELECT tkt_uuid FROM ticket"
900 " WHERE tkt_id=%d", rid);
901 zUrl = mprintf("/tktview/%.20z", zId);
902 break;
903 }
904 }
905 return zUrl;
906 }
907
908 /*
909 ** COMMAND: test-search-stext
910 **
911 ** Usage: fossil test-search-stext TYPE ARG1 ARG2
912 */
913 void test_search_stext(void){
914 Blob out;
915 char *zUrl;
916 db_find_and_open_repository(0,0);
917 if( g.argc!=5 ) usage("TYPE RID NAME");
918 search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out);
919 zUrl = search_url(g.argv[2][0], atoi(g.argv[3]), g.argv[4]);
920 fossil_print("%s\n%z\n",blob_str(&out),zUrl);
921 blob_reset(&out);
922 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
--- src/search.c
+++ src/search.c
@@ -45,14 +45,15 @@
45 char *zPattern; /* The search pattern */
46 char *zMarkBegin; /* Start of a match */
47 char *zMarkEnd; /* End of a match */
48 char *zMarkGap; /* A gap between two matches */
49 unsigned fSrchFlg; /* Flags */
50 int iScore; /* Score of the last match attempt */
51 Blob snip; /* Snippet for the most recent match */
52 };
53
54 #define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */
 
55 #define SRCHFLG_STATIC 0x04 /* The static gSearch object */
56
57 #endif
58
59 /*
@@ -92,10 +93,11 @@
93 if( p ){
94 fossil_free(p->zPattern);
95 fossil_free(p->zMarkBegin);
96 fossil_free(p->zMarkEnd);
97 fossil_free(p->zMarkGap);
98 if( p->iScore ) blob_reset(&p->snip);
99 memset(p, 0, sizeof(*p));
100 if( p!=&gSearch ) fossil_free(p);
101 }
102 }
103
@@ -123,10 +125,11 @@
125 p->zPattern = z = mprintf("%s", zPattern);
126 p->zMarkBegin = mprintf("%s", zMarkBegin);
127 p->zMarkEnd = mprintf("%s", zMarkEnd);
128 p->zMarkGap = mprintf("%s", zMarkGap);
129 p->fSrchFlg = fSrchFlg;
130 blob_init(&p->snip, 0, 0);
131 while( *z && p->nTerm<SEARCH_MAX_TERM ){
132 while( *z && !ISALNUM(*z) ){ z++; }
133 if( *z==0 ) break;
134 p->a[p->nTerm].z = z;
135 for(i=1; ISALNUM(z[i]); i++){}
@@ -156,24 +159,26 @@
159 }
160 }
161
162 /*
163 ** Compare a search pattern against one or more input strings which
164 ** collectively comprise a document. Return a match score. Any
165 ** postive value means there was a match. Zero means that one or
166 ** more terms are missing.
167 **
168 ** The score and a snippet are record for future use.
169 **
170 ** Scoring:
171 ** * All terms must match at least once or the score is zero
172 ** * One point for each matching term
173 ** * Extra points if consecutive words of the pattern are consecutive
174 ** in the document
175 */
176 static int search_match(
177 Search *p, /* Search pattern and flags */
178 int nDoc, /* Number of strings in this document */
179 const char **azDoc /* Text of each string */
 
180 ){
181 int score; /* Final score */
182 int i; /* Offset into current document */
183 int ii; /* Loop counter */
184 int j; /* Loop over search terms */
@@ -226,18 +231,17 @@
231 /* Finished search all documents.
232 ** Every term must be seen or else the score is zero
233 */
234 score = 1;
235 for(j=0; j<p->nTerm; j++) score *= anMatch[j];
236 blob_reset(&p->snip);
237 p->iScore = score;
238 if( score==0 ) return score;
239
240
241 /* Prepare a snippet that describes the matching text.
242 */
 
 
 
243 while(1){
244 int iOfst;
245 int iTail;
246 int iBest;
247 for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){}
@@ -272,11 +276,11 @@
276 if( iOfst<0 ) iOfst = 0;
277 while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--;
278 while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++;
279 for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){}
280 while( ISALNUM(zDoc[iTail]) ) iTail++;
281 if( iOfst>0 || wantGap ) blob_append(&p->snip, p->zMarkGap, -1);
282 wantGap = zDoc[iTail]!=0;
283 zDoc += iOfst;
284 iTail -= iOfst;
285
286 /* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */
@@ -285,53 +289,51 @@
289 for(j=0; j<p->nTerm; j++){
290 int n = p->a[j].n;
291 if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0
292 && (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*')
293 ){
294 snippet_text_append(p, &p->snip, zDoc, i);
295 zDoc += i;
296 iTail -= i;
297 blob_append(&p->snip, p->zMarkBegin, -1);
298 if( p->a[j].z[n]=='*' ){
299 while( ISALNUM(zDoc[n]) ) n++;
300 }
301 snippet_text_append(p, &p->snip, zDoc, n);
302 zDoc += n;
303 iTail -= n;
304 blob_append(&p->snip, p->zMarkEnd, -1);
305 i = -1;
306 break;
307 } /* end-if */
308 } /* end for(j) */
309 if( j<p->nTerm ){
310 while( ISALNUM(zDoc[i]) && i<iTail ){ i++; }
311 }
312 } /* end for(i) */
313 snippet_text_append(p, &p->snip, zDoc, iTail);
314 }
315 if( wantGap ) blob_append(&p->snip, p->zMarkGap, -1);
316 return score;
317 }
318
319 /*
320 ** COMMAND: test-match
321 **
322 ** Usage: fossil test-match SEARCHSTRING FILE1 FILE2 ...
323 */
324 void test_match_cmd(void){
325 Search *p;
326 int i;
327 Blob x;
 
328 int score;
329 char *zDoc;
330 int flg = 0;
331 char *zBegin = (char*)find_option("begin",0,1);
332 char *zEnd = (char*)find_option("end",0,1);
333 char *zGap = (char*)find_option("gap",0,1);
334 if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML;
 
335 if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC;
336 verify_all_options();
337 if( g.argc<4 ) usage("SEARCHSTRING FILE1...");
338 if( zBegin==0 ) zBegin = "[[";
339 if( zEnd==0 ) zEnd = "]]";
@@ -338,18 +340,18 @@
340 if( zGap==0 ) zGap = " ... ";
341 p = search_init(g.argv[2], zBegin, zEnd, zGap, flg);
342 for(i=3; i<g.argc; i++){
343 blob_read_from_file(&x, g.argv[i]);
344 zDoc = blob_str(&x);
345 score = search_match(p, 1, (const char**)&zDoc);
346 fossil_print("%s: %d\n", g.argv[i], p->iScore);
347 blob_reset(&x);
348 if( score ){
349 fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&p->snip), '=');
 
350 }
351 }
352 search_end(p);
353 }
354
355 /*
356 ** An SQL function to initialize the global search pattern:
357 **
@@ -361,12 +363,12 @@
363 sqlite3_context *context,
364 int argc,
365 sqlite3_value **argv
366 ){
367 const char *zPattern = 0;
368 const char *zBegin = "<mark>";
369 const char *zEnd = "</mark>";
370 const char *zGap = " ... ";
371 unsigned int flg = SRCHFLG_HTML;
372 switch( argc ){
373 default:
374 flg = (unsigned int)sqlite3_value_int(argv[4]);
@@ -385,35 +387,45 @@
387 search_end(&gSearch);
388 }
389 }
390
391 /*
392 ** Try to match the input text against the search parameters set up
393 ** by the previous search_init() call. Remember the results globally.
394 ** Return non-zero on a match and zero on a miss.
395 */
396 static void search_match_sqlfunc(
397 sqlite3_context *context,
398 int argc,
399 sqlite3_value **argv
400 ){
401 const char *zSText = (const char*)sqlite3_value_text(argv[0]);
402 int rc;
403 if( zSText==0 ) return;
404 rc = search_match(&gSearch, 1, &zSText);
405 sqlite3_result_int(context, rc);
406 }
407
408 /*
409 ** These SQL functions return the results of the last
410 ** call to the search_match() SQL function.
411 */
412 static void search_score_sqlfunc(
413 sqlite3_context *context,
414 int argc,
415 sqlite3_value **argv
416 ){
417 sqlite3_result_int(context, gSearch.iScore);
418 }
419 static void search_snippet_sqlfunc(
420 sqlite3_context *context,
421 int argc,
422 sqlite3_value **argv
423 ){
424 if( blob_size(&gSearch.snip)>0 ){
425 sqlite3_result_text(context, blob_str(&gSearch.snip), -1, fossil_free);
426 blob_init(&gSearch.snip, 0, 0);
 
 
 
 
 
 
 
427 }
428 }
429
430 /*
431 ** This is an SQLite function that computes the searchable text.
@@ -449,14 +461,18 @@
461 ** Register the "score()" SQL function to score its input text
462 ** using the given Search object. Once this function is registered,
463 ** do not delete the Search object.
464 */
465 void search_sql_setup(sqlite3 *db){
466 static int once = 0;
467 if( once++ ) return;
468 sqlite3_create_function(db, "search_match", 1, SQLITE_UTF8, 0,
469 search_match_sqlfunc, 0, 0);
470 sqlite3_create_function(db, "search_score", 0, SQLITE_UTF8, 0,
471 search_score_sqlfunc, 0, 0);
472 sqlite3_create_function(db, "search_snippet", 0, SQLITE_UTF8, 0,
473 search_snippet_sqlfunc, 0, 0);
474 sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0,
475 search_init_sqlfunc, 0, 0);
476 sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0,
477 search_stext_sqlfunc, 0, 0);
478 sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0,
@@ -551,28 +567,217 @@
567 /*
568 ** Remove bits from srchFlags which are disallowed by either the
569 ** current server configuration or by user permissions.
570 */
571 unsigned int search_restrict(unsigned int srchFlags){
572 if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC);
573 if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT);
574 if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI);
575 if( search_index_exists() ) return srchFlags;
576 if( (srchFlags & SRCH_CKIN)!=0 && db_get_boolean("search-ci",0)==0 ){
577 srchFlags &= ~SRCH_CKIN;
578 }
579 if( (srchFlags & SRCH_DOC)!=0 && db_get_boolean("search-doc",0)==0 ){
 
580 srchFlags &= ~SRCH_DOC;
581 }
582 if( (srchFlags & SRCH_TKT)!=0 && db_get_boolean("search-tkt",0)==0 ){
 
583 srchFlags &= ~SRCH_TKT;
584 }
585 if( (srchFlags & SRCH_WIKI)!=0 && db_get_boolean("search-wiki",0)==0 ){
 
586 srchFlags &= ~SRCH_WIKI;
587 }
588 return srchFlags;
589 }
590
591 /*
592 ** When this routine is called, there already exists a table
593 **
594 ** x(label,url,score,date,snip).
595 **
596 ** And the srchFlags parameter has been validated. This routine
597 ** fills the X table with search results using a full-text scan.
598 **
599 ** The companion indexed scan routine is search_indexed().
600 */
601 static void search_fullscan(
602 const char *zPattern, /* The query pattern */
603 unsigned int srchFlags /* What to search over */
604 ){
605 search_init(zPattern, "<b>", "</b>", " ... ",
606 SRCHFLG_STATIC|SRCHFLG_HTML);
607 if( (srchFlags & SRCH_DOC)!=0 ){
608 char *zDocGlob = db_get("doc-glob","");
609 char *zDocBr = db_get("doc-branch","trunk");
610 if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){
611 db_multi_exec(
612 "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
613 );
614 db_multi_exec(
615 "INSERT INTO x(label,url,score,date,snip)"
616 " SELECT printf('Document: %%s',foci.filename),"
617 " printf('%R/doc/%T/%%s',foci.filename),"
618 " search_score(),"
619 " (SELECT datetime(event.mtime) FROM event"
620 " WHERE objid=symbolic_name_to_rid('trunk')),"
621 " search_snippet()"
622 " FROM foci CROSS JOIN blob"
623 " WHERE checkinID=symbolic_name_to_rid('trunk')"
624 " AND blob.uuid=foci.uuid"
625 " AND search_match(stext('d',blob.rid,foci.filename))"
626 " AND %z",
627 zDocBr, glob_expr("foci.filename", zDocGlob)
628 );
629 }
630 }
631 if( (srchFlags & SRCH_WIKI)!=0 ){
632 db_multi_exec(
633 "WITH wiki(name,rid,mtime) AS ("
634 " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)"
635 " FROM tag, tagxref"
636 " WHERE tag.tagname GLOB 'wiki-*'"
637 " AND tagxref.tagid=tag.tagid"
638 " GROUP BY 1"
639 ")"
640 "INSERT INTO x(label,url,score,date,snip)"
641 " SELECT printf('Wiki: %%s',name),"
642 " printf('%R/wiki?name=%%s',urlencode(name)),"
643 " search_score(),"
644 " datetime(mtime),"
645 " search_snippet()"
646 " FROM wiki"
647 " WHERE search_match(stext('w',rid,name));"
648 );
649 }
650 if( (srchFlags & SRCH_CKIN)!=0 ){
651 db_multi_exec(
652 "WITH ckin(uuid,rid,mtime) AS ("
653 " SELECT blob.uuid, event.objid, event.mtime"
654 " FROM event, blob"
655 " WHERE event.type='ci'"
656 " AND blob.rid=event.objid"
657 ")"
658 "INSERT INTO x(label,url,score,date,snip)"
659 " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime)),"
660 " printf('%R/timeline?c=%%s&n=8&y=ci',uuid),"
661 " search_score(),"
662 " datetime(mtime),"
663 " search_snippet()"
664 " FROM ckin"
665 " WHERE search_match(stext('c',rid,NULL));"
666 );
667 }
668 if( (srchFlags & SRCH_TKT)!=0 ){
669 db_multi_exec(
670 "INSERT INTO x(label,url,score, date,snip)"
671 " SELECT printf('Ticket [%%.17s] on %%s',"
672 "tkt_uuid,datetime(tkt_mtime)),"
673 " printf('%R/tktview/%%.20s',tkt_uuid),"
674 " search_score(),"
675 " datetime(tkt_mtime),"
676 " search_snippet()"
677 " FROM ticket"
678 " WHERE search_match(stext('t',tkt_id,NULL));"
679 );
680 }
681 }
682
683 /*
684 ** Implemenation of the rank() function used with rank(matchinfo(*,'pcsx')).
685 */
686 static void search_rank_sqlfunc(
687 sqlite3_context *context,
688 int argc,
689 sqlite3_value **argv
690 ){
691 const unsigned *aVal = (unsigned int*)sqlite3_value_blob(argv[0]);
692 int nVal = sqlite3_value_bytes(argv[0])/4;
693 int nTerm; /* Number of search terms in the query */
694 int i; /* Loop counter */
695 double r = 1.0; /* Score */
696
697 if( nVal<6 ) return;
698 if( aVal[1]!=1 ) return;
699 nTerm = aVal[0];
700 r *= 1<<((30*(aVal[2]-1))/nTerm);
701 for(i=1; i<=nTerm; i++){
702 int hits_this_row = aVal[3*i];
703 int hits_all_rows = aVal[3*i+1];
704 int rows_with_hit = aVal[3*i+2];
705 double avg_hits_per_row = (double)hits_all_rows/(double)rows_with_hit;
706 r *= hits_this_row/avg_hits_per_row;
707 }
708 #define SEARCH_DEBUG_RANK 0
709 #if SEARCH_DEBUG_RANK
710 {
711 Blob x;
712 blob_init(&x,0,0);
713 blob_appendf(&x,"%08x", (int)r);
714 for(i=0; i<nVal; i++){
715 blob_appendf(&x," %d", aVal[i]);
716 }
717 blob_appendf(&x," r=%g", r);
718 sqlite3_result_text(context, blob_str(&x), -1, fossil_free);
719 }
720 #else
721 sqlite3_result_double(context, r);
722 #endif
723 }
724
725 /*
726 ** When this routine is called, there already exists a table
727 **
728 ** x(label,url,score,date,snip).
729 **
730 ** And the srchFlags parameter has been validated. This routine
731 ** fills the X table with search results using a index scan.
732 **
733 ** The companion full-text scan routine is search_fullscan().
734 */
735 static void search_indexed(
736 const char *zPattern, /* The query pattern */
737 unsigned int srchFlags /* What to search over */
738 ){
739 Blob sql;
740 if( srchFlags==0 ) return;
741 sqlite3_create_function(g.db, "rank", 1, SQLITE_UTF8, 0,
742 search_rank_sqlfunc, 0, 0);
743 blob_init(&sql, 0, 0);
744 blob_appendf(&sql,
745 "INSERT INTO x(label,url,score,date,snip) "
746 " SELECT ftsdocs.label,"
747 " ftsdocs.url,"
748 " rank(matchinfo(ftsidx,'pcsx')),"
749 " datetime(ftsdocs.mtime),"
750 " snippet(ftsidx,'<mark>','</mark>')"
751 " FROM ftsidx, ftsdocs"
752 " WHERE ftsidx MATCH %Q"
753 " AND ftsdocs.rowid=ftsidx.docid",
754 zPattern
755 );
756 if( srchFlags!=SRCH_ALL ){
757 const char *zSep = " AND (";
758 static const struct { unsigned m; char c; } aMask[] = {
759 { SRCH_CKIN, 'c' },
760 { SRCH_DOC, 'd' },
761 { SRCH_TKT, 't' },
762 { SRCH_WIKI, 'w' },
763 };
764 int i;
765 for(i=0; i<ArraySize(aMask); i++){
766 if( srchFlags & aMask[i].m ){
767 blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c);
768 zSep = " OR ";
769 }
770 }
771 blob_append(&sql,")",1);
772 }
773 db_multi_exec("%s",blob_str(&sql)/*safe-for-%s*/);
774 #if SEARCH_DEBUG_RANK
775 db_multi_exec("UPDATE x SET label=printf('%%s (score=%%s)',label,score)");
776 #endif
777 }
778
779
780 /*
781 ** This routine generates web-page output for a search operation.
782 ** Other web-pages can invoke this routine to add search results
783 ** in the middle of the page.
@@ -588,91 +793,32 @@
793
794 srchFlags = search_restrict(srchFlags);
795 if( srchFlags==0 ) return 0;
796 search_sql_setup(g.db);
797 add_content_sql_commands(g.db);
798 db_multi_exec(
799 "CREATE TEMP TABLE x(label,url,score,date,snip);"
800 );
801 if( !search_index_exists() ){
802 search_fullscan(zPattern, srchFlags);
803 }else{
804 search_update_index(srchFlags);
805 search_indexed(zPattern, srchFlags);
806 }
807 db_prepare(&q, "SELECT url, snip, label"
808 " FROM x"
809 " ORDER BY score DESC, date DESC;");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810 while( db_step(&q)==SQLITE_ROW ){
811 const char *zUrl = db_column_text(&q, 0);
812 const char *zSnippet = db_column_text(&q, 1);
813 const char *zLabel = db_column_text(&q, 2);
814 if( nRow==0 ){
815 @ <ol>
816 }
817 nRow++;
818 @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br>
819 @ <span class='snippet'>%s(zSnippet)</span></li>
820 }
821 db_finalize(&q);
822 if( nRow ){
823 @ </ol>
824 }
@@ -789,18 +935,18 @@
935 ** zName Name of the object being searched.
936 */
937 void search_stext(
938 char cType, /* Type of document */
939 int rid, /* BLOB.RID or TAG.TAGID value for document */
940 const char *zName, /* Auxiliary information */
941 Blob *pOut /* OUT: Initialize to the search text */
942 ){
943 blob_init(pOut, 0, 0);
944 switch( cType ){
945 case 'd': { /* Documents */
946 Blob doc;
947 content_get(rid, &doc);
948 blob_to_utf8_no_bom(&doc, 0);
949 get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
950 blob_reset(&doc);
951 break;
952 }
@@ -860,63 +1006,355 @@
1006 break;
1007 }
1008 }
1009 }
1010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1011 /*
1012 ** COMMAND: test-search-stext
1013 **
1014 ** Usage: fossil test-search-stext TYPE ARG1 ARG2
1015 */
1016 void test_search_stext(void){
1017 Blob out;
 
1018 db_find_and_open_repository(0,0);
1019 if( g.argc!=5 ) usage("TYPE RID NAME");
1020 search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out);
1021 fossil_print("%s\n",blob_str(&out));
 
1022 blob_reset(&out);
1023 }
1024
1025 /* The schema for the full-text index
1026 */
1027 static const char zFtsSchema[] =
1028 @ -- One entry for each possible search result
1029 @ CREATE TABLE IF NOT EXISTS "%w".ftsdocs(
1030 @ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid
1031 @ type CHAR(1), -- Type of document
1032 @ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document
1033 @ name TEXT, -- Additional document description
1034 @ idxed BOOLEAN, -- True if currently in the index
1035 @ label TEXT, -- Label to print on search results
1036 @ url TEXT, -- URL to access this document
1037 @ mtime DATE, -- Date when document created
1038 @ UNIQUE(type,rid)
1039 @ );
1040 @ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0;
1041 @ CREATE INDEX "%w".ftsdocName ON ftsdocs(name) WHERE type='w';
1042 @ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS
1043 @ SELECT rowid, type, rid, name, idxed, label, url, mtime,
1044 @ stext(type,rid,name) AS 'stext'
1045 @ FROM ftsdocs;
1046 @ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx
1047 @ USING fts4(content="ftscontent", stext);
1048 ;
1049 static const char zFtsDrop[] =
1050 @ DROP TABLE IF EXISTS "%w".ftsidx;
1051 @ DROP VIEW IF EXISTS "%w".ftscontent;
1052 @ DROP TABLE IF EXISTS "%w".ftsdocs;
1053 ;
1054
1055 /*
1056 ** Create or drop the tables associated with a full-text index.
1057 */
1058 void search_create_index(void){
1059 const char *zDb = db_name("repository");
1060 search_sql_setup(g.db);
1061 db_multi_exec(zFtsSchema/*works-like:"%w%w%w%w%w"*/,
1062 zDb, zDb, zDb, zDb, zDb);
1063 }
1064 void search_drop_index(void){
1065 const char *zDb = db_name("repository");
1066 db_multi_exec(zFtsDrop/*works-like:"%w%w%w"*/, zDb, zDb, zDb);
1067 }
1068
1069 /*
1070 ** Return true if the full-text search index exists
1071 */
1072 int search_index_exists(void){
1073 static int fExists = -1;
1074 if( fExists<0 ) fExists = db_table_exists("repository","ftsdocs");
1075 return fExists;
1076 }
1077
1078 /*
1079 ** Fill the FTSDOCS table with unindexed entries for everything
1080 ** in the repository. This uses INSERT OR IGNORE so entries already
1081 ** in FTSDOCS are unchanged.
1082 */
1083 void search_fill_index(void){
1084 if( !search_index_exists() ) return;
1085 search_sql_setup(g.db);
1086 db_multi_exec(
1087 "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
1088 " SELECT 'c', objid, 0 FROM event WHERE type='ci';"
1089 );
1090 db_multi_exec(
1091 "WITH latest_wiki(rid,name,mtime) AS ("
1092 " SELECT tagxref.rid, substr(tag.tagname,6), max(tagxref.mtime)"
1093 " FROM tag, tagxref"
1094 " WHERE tag.tagname GLOB 'wiki-*'"
1095 " AND tagxref.tagid=tag.tagid"
1096 " AND tagxref.value>0"
1097 " GROUP BY 2"
1098 ") INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)"
1099 " SELECT 'w', rid, name, 0 FROM latest_wiki;"
1100 );
1101 db_multi_exec(
1102 "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
1103 " SELECT 't', tkt_id, 0 FROM ticket;"
1104 );
1105 }
1106
1107 /*
1108 ** The document described by cType,rid,zName is about to be added or
1109 ** updated. If the document has already been indexed, then unindex it
1110 ** now while we still have access to the old content. Add the document
1111 ** to the queue of documents that need to be indexed or reindexed.
1112 */
1113 void search_doc_touch(char cType, int rid, const char *zName){
1114 if( search_index_exists() ){
1115 char zType[2];
1116 zType[0] = cType;
1117 zType[1] = 0;
1118 db_multi_exec(
1119 "DELETE FROM ftsidx WHERE docid IN"
1120 " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)",
1121 zType, rid
1122 );
1123 db_multi_exec(
1124 "REPLACE INTO ftsdocs(type,rid,name,idxed)"
1125 " VALUES(%Q,%d,%Q,0)",
1126 zType, rid, zName
1127 );
1128 if( cType=='w' ){
1129 db_multi_exec(
1130 "DELETE FROM ftsidx WHERE docid IN"
1131 " (SELECT rowid FROM ftsdocs WHERE type='w' AND name=%Q AND idxed)",
1132 zName
1133 );
1134 db_multi_exec(
1135 "DELETE FROM ftsdocs WHERE type='w' AND name=%Q AND rid!=%d",
1136 zName, rid
1137 );
1138 }
1139 }
1140 }
1141
1142 /*
1143 ** If the doc-glob and doc-br settings are valid for document search
1144 ** and if the latest check-in on doc-br is in the unindexed set of
1145 ** check-ins, then update all 'd' entries in FTSDOCS that have
1146 ** changed.
1147 */
1148 static void search_update_doc_index(void){
1149 const char *zDocBr = db_get("doc-branch","trunk");
1150 int ckid = zDocBr ? symbolic_name_to_rid(zDocBr,"ci") : 0;
1151 double rTime;
1152 char *zBrUuid;
1153 if( ckid==0 ) return;
1154 if( !db_exists("SELECT 1 FROM ftsdocs WHERE type='c' AND rid=%d"
1155 " AND NOT idxed", ckid) ) return;
1156
1157 /* If we get this far, it means that changes to 'd' entries are
1158 ** required. */
1159 rTime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", ckid);
1160 zBrUuid = db_text("","SELECT substr(uuid,1,20) FROM blob WHERE rid=%d",ckid);
1161 db_multi_exec(
1162 "CREATE TEMP TABLE current_docs(rid INTEGER PRIMARY KEY, name);"
1163 "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;"
1164 "INSERT OR IGNORE INTO current_docs(rid, name)"
1165 " SELECT blob.rid, foci.filename FROM foci, blob"
1166 " WHERE foci.checkinID=%d AND blob.uuid=foci.uuid"
1167 " AND %z",
1168 ckid, glob_expr("foci.filename", db_get("doc-glob",""))
1169 );
1170 db_multi_exec(
1171 "DELETE FROM ftsidx WHERE docid IN"
1172 " (SELECT rowid FROM ftsdocs WHERE type='d'"
1173 " AND rid NOT IN (SELECT rid FROM current_docs))"
1174 );
1175 db_multi_exec(
1176 "DELETE FROM ftsdocs WHERE type='d'"
1177 " AND rid NOT IN (SELECT rid FROM current_docs)"
1178 );
1179 db_multi_exec(
1180 "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed,label,url,mtime)"
1181 " SELECT 'd', rid, name, 0,"
1182 " printf('Document: %%s',name),"
1183 " printf('/doc/%q/%%s',urlencode(name)),"
1184 " %.17g"
1185 " FROM current_docs",
1186 zBrUuid, rTime
1187 );
1188 db_multi_exec(
1189 "INSERT INTO ftsidx(docid,stext)"
1190 " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed"
1191 );
1192 db_multi_exec(
1193 "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed"
1194 );
1195 }
1196
1197 /*
1198 ** Deal with all of the unindexed 'c' terms in FTSDOCS
1199 */
1200 static void search_update_checkin_index(void){
1201 db_multi_exec(
1202 "INSERT INTO ftsidx(docid,stext)"
1203 " SELECT rowid, stext('c',rid,NULL) FROM ftsdocs"
1204 " WHERE type='c' AND NOT idxed;"
1205 );
1206 db_multi_exec(
1207 "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1208 " SELECT ftsdocs.rowid, 1, 'c', ftsdocs.rid, NULL,"
1209 " printf('Check-in [%%.16s] on %%s',blob.uuid,datetime(event.mtime)),"
1210 " printf('/timeline?y=ci&n=9&c=%%.20s',blob.uuid),"
1211 " event.mtime"
1212 " FROM ftsdocs, event, blob"
1213 " WHERE ftsdocs.type='c' AND NOT ftsdocs.idxed"
1214 " AND event.objid=ftsdocs.rid"
1215 " AND blob.rid=ftsdocs.rid"
1216 );
1217 }
1218
1219 /*
1220 ** Deal with all of the unindexed 't' terms in FTSDOCS
1221 */
1222 static void search_update_ticket_index(void){
1223 db_multi_exec(
1224 "INSERT INTO ftsidx(docid,stext)"
1225 " SELECT rowid, stext('t',rid,NULL) FROM ftsdocs"
1226 " WHERE type='t' AND NOT idxed;"
1227 );
1228 if( db_changes()==0 ) return;
1229 db_multi_exec(
1230 "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1231 " SELECT ftsdocs.rowid, 1, 't', ftsdocs.rid, NULL,"
1232 " printf('Ticket [%%.16s] on %%s',tkt_uuid,datetime(tkt_mtime)),"
1233 " printf('/tktview/%%.20s',tkt_uuid),"
1234 " tkt_mtime"
1235 " FROM ftsdocs, ticket"
1236 " WHERE ftsdocs.type='t' AND NOT ftsdocs.idxed"
1237 " AND ticket.tkt_id=ftsdocs.rid"
1238 );
1239 }
1240
1241 /*
1242 ** Deal with all of the unindexed 'w' terms in FTSDOCS
1243 */
1244 static void search_update_wiki_index(void){
1245 db_multi_exec(
1246 "INSERT INTO ftsidx(docid,stext)"
1247 " SELECT rowid, stext('w',rid,NULL) FROM ftsdocs"
1248 " WHERE type='w' AND NOT idxed;"
1249 );
1250 if( db_changes()==0 ) return;
1251 db_multi_exec(
1252 "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)"
1253 " SELECT ftsdocs.rowid, 1, 'w', ftsdocs.rid, ftsdocs.name,"
1254 " 'Wiki: '||ftsdocs.name,"
1255 " '/wiki?name='||urlencode(ftsdocs.name),"
1256 " tagxref.mtime"
1257 " FROM ftsdocs, tagxref"
1258 " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed"
1259 " AND tagxref.rid=ftsdocs.rid"
1260 );
1261 }
1262
1263 /*
1264 ** Deal with all of the unindexed entries in the FTSDOCS table - that
1265 ** is to say, all the entries with FTSDOCS.IDXED=0. Add them to the
1266 ** index.
1267 */
1268 void search_update_index(unsigned int srchFlags){
1269 if( !search_index_exists() ) return;
1270 if( !db_exists("SELECT 1 FROM ftsdocs WHERE NOT idxed") ) return;
1271 search_sql_setup(g.db);
1272 if( srchFlags & (SRCH_CKIN|SRCH_DOC) ){
1273 search_update_doc_index();
1274 search_update_checkin_index();
1275 }
1276 if( srchFlags & SRCH_TKT ){
1277 search_update_ticket_index();
1278 }
1279 if( srchFlags & SRCH_WIKI ){
1280 search_update_wiki_index();
1281 }
1282 }
1283
1284 /*
1285 ** COMMAND: test-fts
1286 */
1287 void test_fts_cmd(void){
1288 char *zSubCmd;
1289 int i, n;
1290 static const struct { int iCmd; const char *z; } aCmd[] = {
1291 { 1, "create" },
1292 { 2, "drop" },
1293 { 3, "exists" },
1294 { 4, "fill" },
1295 { 8, "refill" },
1296 { 5, "pending" },
1297 { 7, "update" },
1298 };
1299 db_find_and_open_repository(0, 0);
1300 if( g.argc<3 ) usage("SUBCMD ...");
1301 zSubCmd = g.argv[2];
1302 n = (int)strlen(zSubCmd);
1303 for(i=0; i<ArraySize(aCmd); i++){
1304 if( fossil_strncmp(aCmd[i].z, zSubCmd, n)==0 ) break;
1305 }
1306 if( i>=ArraySize(aCmd) ){
1307 Blob all;
1308 blob_init(&all,0,0);
1309 for(i=0; i<ArraySize(aCmd); i++) blob_appendf(&all, " %s", aCmd[i].z);
1310 fossil_fatal("unknown \"%s\" - should be:%s", zSubCmd, blob_str(&all));
1311 return;
1312 }
1313 db_begin_transaction();
1314 switch( aCmd[i].iCmd ){
1315 case 1: { assert( fossil_strncmp(zSubCmd, "create", n)==0 );
1316 search_create_index();
1317 break;
1318 }
1319 case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 );
1320 search_drop_index();
1321 break;
1322 }
1323 case 3: { assert( fossil_strncmp(zSubCmd, "exists", n)==0 );
1324 fossil_print("search_index_exists() = %d\n", search_index_exists());
1325 break;
1326 }
1327 case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 );
1328 search_fill_index();
1329 break;
1330 }
1331 case 8: { assert( fossil_strncmp(zSubCmd, "refill", n)==0 );
1332 search_drop_index();
1333 search_create_index();
1334 search_fill_index();
1335 break;
1336 }
1337 case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 );
1338 Stmt q;
1339 if( !search_index_exists() ) break;
1340 db_prepare(&q, "SELECT rowid,type,rid,quote(name) FROM ftsdocs"
1341 " WHERE NOT idxed");
1342 while( db_step(&q)==SQLITE_ROW ){
1343 fossil_print("%6d: %s %6d %s\n",
1344 db_column_int(&q, 0),
1345 db_column_text(&q, 1),
1346 db_column_int(&q, 2),
1347 db_column_text(&q, 3)
1348 );
1349 }
1350 db_finalize(&q);
1351 break;
1352 }
1353 case 7: { assert( fossil_strncmp(zSubCmd, "update", n)==0 );
1354 search_update_index(SRCH_ALL);
1355 break;
1356 }
1357
1358 }
1359 db_end_transaction(0);
1360 }
1361
+19
--- src/setup.c
+++ src/setup.c
@@ -2206,8 +2206,27 @@
22062206
onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0);
22072207
@ <br>
22082208
onoff_attribute("Search Wiki","search-wiki", "sw", 0, 0);
22092209
@ <hr/>
22102210
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
2211
+ @ <hr/>
2212
+ if( P("fts0") ){
2213
+ search_drop_index();
2214
+ }else if( P("fts1") ){
2215
+ search_drop_index();
2216
+ search_create_index();
2217
+ search_fill_index();
2218
+ search_update_index(SRCH_ALL);
2219
+ }
2220
+ if( search_index_exists() ){
2221
+ @ <p>Currently using an SQLite FTS4 search index. This makes search
2222
+ @ run faster, especially on large repositories, but takes up space.</p>
2223
+ @ <p><input type="submit" name="fts0" value="Delete The Full-Text Index">
2224
+ }else{
2225
+ @ <p>The SQLite FTS4 search index is disabled. All searching will be
2226
+ @ a full-text scan. This usually works fine, but can be slow for
2227
+ @ larger repositories.</p>
2228
+ @ <p><input type="submit" name="fts1" value="Create A Full-Text Index">
2229
+ }
22112230
@ </div></form>
22122231
style_footer();
22132232
}
22142233
--- src/setup.c
+++ src/setup.c
@@ -2206,8 +2206,27 @@
2206 onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0);
2207 @ <br>
2208 onoff_attribute("Search Wiki","search-wiki", "sw", 0, 0);
2209 @ <hr/>
2210 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2211 @ </div></form>
2212 style_footer();
2213 }
2214
--- src/setup.c
+++ src/setup.c
@@ -2206,8 +2206,27 @@
2206 onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0);
2207 @ <br>
2208 onoff_attribute("Search Wiki","search-wiki", "sw", 0, 0);
2209 @ <hr/>
2210 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
2211 @ <hr/>
2212 if( P("fts0") ){
2213 search_drop_index();
2214 }else if( P("fts1") ){
2215 search_drop_index();
2216 search_create_index();
2217 search_fill_index();
2218 search_update_index(SRCH_ALL);
2219 }
2220 if( search_index_exists() ){
2221 @ <p>Currently using an SQLite FTS4 search index. This makes search
2222 @ run faster, especially on large repositories, but takes up space.</p>
2223 @ <p><input type="submit" name="fts0" value="Delete The Full-Text Index">
2224 }else{
2225 @ <p>The SQLite FTS4 search index is disabled. All searching will be
2226 @ a full-text scan. This usually works fine, but can be slow for
2227 @ larger repositories.</p>
2228 @ <p><input type="submit" name="fts1" value="Create A Full-Text Index">
2229 }
2230 @ </div></form>
2231 style_footer();
2232 }
2233
+31 -2
--- src/style.c
+++ src/style.c
@@ -1125,10 +1125,15 @@
11251125
},
11261126
{ "th.sort.desc:after",
11271127
"Descending sort column marker",
11281128
@ content: '\2191';
11291129
},
1130
+ { "span.snippet>mark",
1131
+ "Search markup",
1132
+ @ background-color: inherit;
1133
+ @ font-weight: bold;
1134
+ },
11301135
{ 0,
11311136
0,
11321137
0
11331138
}
11341139
};
@@ -1149,23 +1154,47 @@
11491154
);
11501155
}
11511156
}
11521157
}
11531158
1159
+/*
1160
+** Search string zHaystack for zNeedle. zNeedle must be an isolated
1161
+** word with space or punctuation on either size.
1162
+**
1163
+** Return true if found. Return false if not found
1164
+*/
1165
+static int containsString(const char *zHaystack, const char *zNeedle){
1166
+ char *z;
1167
+ int n;
1168
+
1169
+ while( zHaystack[0] ){
1170
+ z = strstr(zHaystack, zNeedle);
1171
+ if( z==0 ) return 0;
1172
+ n = (int)strlen(zNeedle);
1173
+ if( (z==zHaystack || !fossil_isalnum(z[-1])) && !fossil_isalnum(z[n]) ){
1174
+ return 1;
1175
+ }
1176
+ zHaystack = z + n;
1177
+ }
1178
+ return 0;
1179
+}
1180
+
1181
+
11541182
/*
11551183
** WEBPAGE: style.css
11561184
*/
11571185
void page_style_css(void){
11581186
Blob css;
11591187
int i;
11601188
11611189
cgi_set_content_type("text/css");
1162
- blob_init(&css, db_get("css",(char*)builtin_text("skins/default/css.txt")), -1);
1190
+ blob_init(&css,db_get("css",(char*)builtin_text("skins/default/css.txt")),-1);
11631191
11641192
/* add special missing definitions */
11651193
for(i=1; cssDefaultList[i].elementClass; i++){
1166
- if( strstr(blob_str(&css), cssDefaultList[i].elementClass)==0 ){
1194
+ char *z = blob_str(&css);
1195
+ if( !containsString(z, cssDefaultList[i].elementClass) ){
11671196
blob_appendf(&css, "/* %s */\n%s {\n%s}\n",
11681197
cssDefaultList[i].comment,
11691198
cssDefaultList[i].elementClass,
11701199
cssDefaultList[i].value);
11711200
}
11721201
--- src/style.c
+++ src/style.c
@@ -1125,10 +1125,15 @@
1125 },
1126 { "th.sort.desc:after",
1127 "Descending sort column marker",
1128 @ content: '\2191';
1129 },
 
 
 
 
 
1130 { 0,
1131 0,
1132 0
1133 }
1134 };
@@ -1149,23 +1154,47 @@
1149 );
1150 }
1151 }
1152 }
1153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1154 /*
1155 ** WEBPAGE: style.css
1156 */
1157 void page_style_css(void){
1158 Blob css;
1159 int i;
1160
1161 cgi_set_content_type("text/css");
1162 blob_init(&css, db_get("css",(char*)builtin_text("skins/default/css.txt")), -1);
1163
1164 /* add special missing definitions */
1165 for(i=1; cssDefaultList[i].elementClass; i++){
1166 if( strstr(blob_str(&css), cssDefaultList[i].elementClass)==0 ){
 
1167 blob_appendf(&css, "/* %s */\n%s {\n%s}\n",
1168 cssDefaultList[i].comment,
1169 cssDefaultList[i].elementClass,
1170 cssDefaultList[i].value);
1171 }
1172
--- src/style.c
+++ src/style.c
@@ -1125,10 +1125,15 @@
1125 },
1126 { "th.sort.desc:after",
1127 "Descending sort column marker",
1128 @ content: '\2191';
1129 },
1130 { "span.snippet>mark",
1131 "Search markup",
1132 @ background-color: inherit;
1133 @ font-weight: bold;
1134 },
1135 { 0,
1136 0,
1137 0
1138 }
1139 };
@@ -1149,23 +1154,47 @@
1154 );
1155 }
1156 }
1157 }
1158
1159 /*
1160 ** Search string zHaystack for zNeedle. zNeedle must be an isolated
1161 ** word with space or punctuation on either size.
1162 **
1163 ** Return true if found. Return false if not found
1164 */
1165 static int containsString(const char *zHaystack, const char *zNeedle){
1166 char *z;
1167 int n;
1168
1169 while( zHaystack[0] ){
1170 z = strstr(zHaystack, zNeedle);
1171 if( z==0 ) return 0;
1172 n = (int)strlen(zNeedle);
1173 if( (z==zHaystack || !fossil_isalnum(z[-1])) && !fossil_isalnum(z[n]) ){
1174 return 1;
1175 }
1176 zHaystack = z + n;
1177 }
1178 return 0;
1179 }
1180
1181
1182 /*
1183 ** WEBPAGE: style.css
1184 */
1185 void page_style_css(void){
1186 Blob css;
1187 int i;
1188
1189 cgi_set_content_type("text/css");
1190 blob_init(&css,db_get("css",(char*)builtin_text("skins/default/css.txt")),-1);
1191
1192 /* add special missing definitions */
1193 for(i=1; cssDefaultList[i].elementClass; i++){
1194 char *z = blob_str(&css);
1195 if( !containsString(z, cssDefaultList[i].elementClass) ){
1196 blob_appendf(&css, "/* %s */\n%s {\n%s}\n",
1197 cssDefaultList[i].comment,
1198 cssDefaultList[i].elementClass,
1199 cssDefaultList[i].value);
1200 }
1201
+1
--- src/tkt.c
+++ src/tkt.c
@@ -313,10 +313,11 @@
313313
314314
fossil_free(zTag);
315315
getAllTicketFields();
316316
if( haveTicket==0 ) return;
317317
tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
318
+ search_doc_touch('t', tktid, 0);
318319
if( haveTicketChng ){
319320
db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
320321
}
321322
db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
322323
tktid = 0;
323324
--- src/tkt.c
+++ src/tkt.c
@@ -313,10 +313,11 @@
313
314 fossil_free(zTag);
315 getAllTicketFields();
316 if( haveTicket==0 ) return;
317 tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
 
318 if( haveTicketChng ){
319 db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
320 }
321 db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
322 tktid = 0;
323
--- src/tkt.c
+++ src/tkt.c
@@ -313,10 +313,11 @@
313
314 fossil_free(zTag);
315 getAllTicketFields();
316 if( haveTicket==0 ) return;
317 tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
318 search_doc_touch('t', tktid, 0);
319 if( haveTicketChng ){
320 db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
321 }
322 db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
323 tktid = 0;
324
--- win/Makefile.PellesCGMake
+++ win/Makefile.PellesCGMake
@@ -83,11 +83,11 @@
8383
8484
# define the SQLite files, which need special flags on compile
8585
SQLITESRC=sqlite3.c
8686
ORIGSQLITESRC=$(foreach sf,$(SQLITESRC),$(SRCDIR)$(sf))
8787
SQLITEOBJ=$(foreach sf,$(SQLITESRC),$(sf:.c=.obj))
88
-SQLITEDEFINES=-DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_WIN32_NO_ANSI
88
+SQLITEDEFINES=-DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_WIN32_NO_ANSI
8989
9090
# define the SQLite shell files, which need special flags on compile
9191
SQLITESHELLSRC=shell.c
9292
ORIGSQLITESHELLSRC=$(foreach sf,$(SQLITESHELLSRC),$(SRCDIR)$(sf))
9393
SQLITESHELLOBJ=$(foreach sf,$(SQLITESHELLSRC),$(sf:.c=.obj))
9494
--- win/Makefile.PellesCGMake
+++ win/Makefile.PellesCGMake
@@ -83,11 +83,11 @@
83
84 # define the SQLite files, which need special flags on compile
85 SQLITESRC=sqlite3.c
86 ORIGSQLITESRC=$(foreach sf,$(SQLITESRC),$(SRCDIR)$(sf))
87 SQLITEOBJ=$(foreach sf,$(SQLITESRC),$(sf:.c=.obj))
88 SQLITEDEFINES=-DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_WIN32_NO_ANSI
89
90 # define the SQLite shell files, which need special flags on compile
91 SQLITESHELLSRC=shell.c
92 ORIGSQLITESHELLSRC=$(foreach sf,$(SQLITESHELLSRC),$(SRCDIR)$(sf))
93 SQLITESHELLOBJ=$(foreach sf,$(SQLITESHELLSRC),$(sf:.c=.obj))
94
--- win/Makefile.PellesCGMake
+++ win/Makefile.PellesCGMake
@@ -83,11 +83,11 @@
83
84 # define the SQLite files, which need special flags on compile
85 SQLITESRC=sqlite3.c
86 ORIGSQLITESRC=$(foreach sf,$(SQLITESRC),$(SRCDIR)$(sf))
87 SQLITEOBJ=$(foreach sf,$(SQLITESRC),$(sf:.c=.obj))
88 SQLITEDEFINES=-DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_WIN32_NO_ANSI
89
90 # define the SQLite shell files, which need special flags on compile
91 SQLITESHELLSRC=shell.c
92 ORIGSQLITESHELLSRC=$(foreach sf,$(SQLITESHELLSRC),$(SRCDIR)$(sf))
93 SQLITESHELLOBJ=$(foreach sf,$(SQLITESHELLSRC),$(sf:.c=.obj))
94
--- win/Makefile.dmc
+++ win/Makefile.dmc
@@ -24,11 +24,11 @@
2424
CFLAGS = -o
2525
BCC = $(DMDIR)\bin\dmc $(CFLAGS)
2626
TCC = $(DMDIR)\bin\dmc $(CFLAGS) $(DMCDEF) $(SSL) $(INCL)
2727
LIBS = $(DMDIR)\extra\lib\ zlib wsock32 advapi32
2828
29
-SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS
29
+SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4
3030
3131
SHELL_OPTIONS = -Dmain=sqlite3_shell -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=fossil_open -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen
3232
3333
SRC = add_.c allrepo_.c attach_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c doc_.c encode_.c event_.c export_.c file_.c finfo_.c foci_.c fusefs_.c glob_.c graph_.c gzip_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c setup_.c sha1_.c shun_.c sitemap_.c skins_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c
3434
3535
--- win/Makefile.dmc
+++ win/Makefile.dmc
@@ -24,11 +24,11 @@
24 CFLAGS = -o
25 BCC = $(DMDIR)\bin\dmc $(CFLAGS)
26 TCC = $(DMDIR)\bin\dmc $(CFLAGS) $(DMCDEF) $(SSL) $(INCL)
27 LIBS = $(DMDIR)\extra\lib\ zlib wsock32 advapi32
28
29 SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS
30
31 SHELL_OPTIONS = -Dmain=sqlite3_shell -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=fossil_open -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen
32
33 SRC = add_.c allrepo_.c attach_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c doc_.c encode_.c event_.c export_.c file_.c finfo_.c foci_.c fusefs_.c glob_.c graph_.c gzip_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c setup_.c sha1_.c shun_.c sitemap_.c skins_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c
34
35
--- win/Makefile.dmc
+++ win/Makefile.dmc
@@ -24,11 +24,11 @@
24 CFLAGS = -o
25 BCC = $(DMDIR)\bin\dmc $(CFLAGS)
26 TCC = $(DMDIR)\bin\dmc $(CFLAGS) $(DMCDEF) $(SSL) $(INCL)
27 LIBS = $(DMDIR)\extra\lib\ zlib wsock32 advapi32
28
29 SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4
30
31 SHELL_OPTIONS = -Dmain=sqlite3_shell -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=fossil_open -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen
32
33 SRC = add_.c allrepo_.c attach_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c doc_.c encode_.c event_.c export_.c file_.c finfo_.c foci_.c fusefs_.c glob_.c graph_.c gzip_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c setup_.c sha1_.c shun_.c sitemap_.c skins_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c
34
35
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -2024,10 +2024,11 @@
20242024
-DSQLITE_ENABLE_LOCKING_STYLE=0 \
20252025
-DSQLITE_THREADSAFE=0 \
20262026
-DSQLITE_DEFAULT_FILE_FORMAT=4 \
20272027
-DSQLITE_OMIT_DEPRECATED \
20282028
-DSQLITE_ENABLE_EXPLAIN_COMMENTS \
2029
+ -DSQLITE_ENABLE_FTS4 \
20292030
-DSQLITE_WIN32_NO_ANSI \
20302031
-D_HAVE__MINGW_H \
20312032
-DSQLITE_USE_MALLOC_H \
20322033
-DSQLITE_USE_MSIZE
20332034
20342035
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -2024,10 +2024,11 @@
2024 -DSQLITE_ENABLE_LOCKING_STYLE=0 \
2025 -DSQLITE_THREADSAFE=0 \
2026 -DSQLITE_DEFAULT_FILE_FORMAT=4 \
2027 -DSQLITE_OMIT_DEPRECATED \
2028 -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
 
2029 -DSQLITE_WIN32_NO_ANSI \
2030 -D_HAVE__MINGW_H \
2031 -DSQLITE_USE_MALLOC_H \
2032 -DSQLITE_USE_MSIZE
2033
2034
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -2024,10 +2024,11 @@
2024 -DSQLITE_ENABLE_LOCKING_STYLE=0 \
2025 -DSQLITE_THREADSAFE=0 \
2026 -DSQLITE_DEFAULT_FILE_FORMAT=4 \
2027 -DSQLITE_OMIT_DEPRECATED \
2028 -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
2029 -DSQLITE_ENABLE_FTS4 \
2030 -DSQLITE_WIN32_NO_ANSI \
2031 -D_HAVE__MINGW_H \
2032 -DSQLITE_USE_MALLOC_H \
2033 -DSQLITE_USE_MSIZE
2034
2035
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -189,10 +189,11 @@
189189
/DSQLITE_ENABLE_LOCKING_STYLE=0 \
190190
/DSQLITE_THREADSAFE=0 \
191191
/DSQLITE_DEFAULT_FILE_FORMAT=4 \
192192
/DSQLITE_OMIT_DEPRECATED \
193193
/DSQLITE_ENABLE_EXPLAIN_COMMENTS \
194
+ /DSQLITE_ENABLE_FTS4 \
194195
/DSQLITE_WIN32_NO_ANSI
195196
196197
SHELL_OPTIONS = /Dmain=sqlite3_shell \
197198
/DSQLITE_OMIT_LOAD_EXTENSION=1 \
198199
/DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
199200
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -189,10 +189,11 @@
189 /DSQLITE_ENABLE_LOCKING_STYLE=0 \
190 /DSQLITE_THREADSAFE=0 \
191 /DSQLITE_DEFAULT_FILE_FORMAT=4 \
192 /DSQLITE_OMIT_DEPRECATED \
193 /DSQLITE_ENABLE_EXPLAIN_COMMENTS \
 
194 /DSQLITE_WIN32_NO_ANSI
195
196 SHELL_OPTIONS = /Dmain=sqlite3_shell \
197 /DSQLITE_OMIT_LOAD_EXTENSION=1 \
198 /DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
199
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -189,10 +189,11 @@
189 /DSQLITE_ENABLE_LOCKING_STYLE=0 \
190 /DSQLITE_THREADSAFE=0 \
191 /DSQLITE_DEFAULT_FILE_FORMAT=4 \
192 /DSQLITE_OMIT_DEPRECATED \
193 /DSQLITE_ENABLE_EXPLAIN_COMMENTS \
194 /DSQLITE_ENABLE_FTS4 \
195 /DSQLITE_WIN32_NO_ANSI
196
197 SHELL_OPTIONS = /Dmain=sqlite3_shell \
198 /DSQLITE_OMIT_LOAD_EXTENSION=1 \
199 /DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \
200

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button