From: dan Date: Mon, 1 Jun 2015 09:15:20 +0000 (+0000) Subject: Change fts5 expression processing to avoid linear scans of long doclists caused by... X-Git-Tag: version-3.8.11~114^2~22 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=34efc82eed62be840e8bf78564aa35a44f328e8e;p=thirdparty%2Fsqlite.git Change fts5 expression processing to avoid linear scans of long doclists caused by phrases that match specific columns only. FossilOrigin-Name: ec69e09a55b4daf1c40aeaaf9ee95091fe86f5c0 --- diff --git a/ext/fts5/fts5_expr.c b/ext/fts5/fts5_expr.c index 1a5f2887f4..9b3e04a74e 100644 --- a/ext/fts5/fts5_expr.c +++ b/ext/fts5/fts5_expr.c @@ -653,10 +653,8 @@ static int fts5ExprNearNextRowidMatch( Fts5ExprNode *pNode ){ Fts5ExprNearset *pNear = pNode->pNear; - int rc = SQLITE_OK; - int i, j; /* Phrase and token index, respectively */ i64 iLast; /* Lastest rowid any iterator points to */ - int bMatch; /* True if all terms are at the same rowid */ + int rc = SQLITE_OK; /* Initialize iLast, the "lastest" rowid any iterator points to. If the ** iterator skips through rowids in the default ascending order, this means @@ -664,20 +662,24 @@ static int fts5ExprNearNextRowidMatch( ** means the minimum rowid. */ iLast = sqlite3Fts5IterRowid(pNear->apPhrase[0]->aTerm[0].pIter); - do { - bMatch = 1; - for(i=0; inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - for(j=0; jnTerm; j++){ - Fts5IndexIter *pIter = pPhrase->aTerm[j].pIter; - i64 iRowid = sqlite3Fts5IterRowid(pIter); - if( iRowid!=iLast ) bMatch = 0; - if( fts5ExprAdvanceto(pIter, pExpr->bDesc, &iLast, &rc, &pNode->bEof) ){ - return rc; + if( pNear->nPhrase>1 || pNear->apPhrase[0]->nTerm>1 ){ + int i, j; /* Phrase and token index, respectively */ + int bMatch; /* True if all terms are at the same rowid */ + do { + bMatch = 1; + for(i=0; inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + for(j=0; jnTerm; j++){ + Fts5IndexIter *pIter = pPhrase->aTerm[j].pIter; + i64 iRowid = sqlite3Fts5IterRowid(pIter); + if( iRowid!=iLast ) bMatch = 0; + if( fts5ExprAdvanceto(pIter, pExpr->bDesc, &iLast,&rc,&pNode->bEof) ){ + return rc; + } } } - } - }while( bMatch==0 ); + }while( bMatch==0 ); + } pNode->iRowid = iLast; return rc; @@ -738,6 +740,76 @@ static int fts5ExprExtractColset ( return rc; } +static int fts5ExprNearTest( + int *pRc, + Fts5Expr *pExpr, /* Expression that pNear is a part of */ + Fts5ExprNode *pNode /* The "NEAR" node (FTS5_STRING) */ +){ + Fts5ExprNearset *pNear = pNode->pNear; + int rc = *pRc; + + if( pNear->nPhrase==1 && pNear->apPhrase[0]->nTerm==1 ){ + /* If this "NEAR" object is actually a single phrase that consists + ** of a single term only, then grab pointers into the poslist + ** managed by the fts5_index.c iterator object. This is much faster + ** than synthesizing a new poslist the way we have to for more + ** complicated phrase or NEAR expressions. */ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[0]; + Fts5IndexIter *pIter = pPhrase->aTerm[0].pIter; + Fts5ExprColset *pColset = pNear->pColset; + const u8 *pPos; + int nPos; + + if( rc!=SQLITE_OK ) return 0; + rc = sqlite3Fts5IterPoslist(pIter, &pPos, &nPos, &pNode->iRowid); + + /* If the term may match any column, then this must be a match. + ** Return immediately in this case. Otherwise, try to find the + ** part of the poslist that corresponds to the required column. + ** If it can be found, return. If it cannot, the next iteration + ** of the loop will test the next rowid in the database for this + ** term. */ + if( pColset==0 ){ + assert( pPhrase->poslist.nSpace==0 ); + pPhrase->poslist.p = (u8*)pPos; + pPhrase->poslist.n = nPos; + }else if( pColset->nCol==1 ){ + assert( pPhrase->poslist.nSpace==0 ); + pPhrase->poslist.n = fts5ExprExtractCol(&pPos, nPos, pColset->aiCol[0]); + pPhrase->poslist.p = (u8*)pPos; + }else if( rc==SQLITE_OK ){ + rc = fts5ExprExtractColset(pColset, pPos, nPos, &pPhrase->poslist); + } + + *pRc = rc; + return (pPhrase->poslist.n>0); + }else{ + int i; + + /* Check that each phrase in the nearset matches the current row. + ** Populate the pPhrase->poslist buffers at the same time. If any + ** phrase is not a match, break out of the loop early. */ + for(i=0; rc==SQLITE_OK && inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + if( pPhrase->nTerm>1 || pNear->pColset ){ + int bMatch = 0; + rc = fts5ExprPhraseIsMatch(pExpr, pNear->pColset, pPhrase, &bMatch); + if( bMatch==0 ) break; + }else{ + rc = sqlite3Fts5IterPoslistBuffer( + pPhrase->aTerm[0].pIter, &pPhrase->poslist + ); + } + } + + *pRc = rc; + if( i==pNear->nPhrase && (i==1 || fts5ExprNearIsMatch(pRc, pNear)) ){ + return 1; + } + } + + return 0; +} /* ** Argument pNode points to a NEAR node. All individual term iterators @@ -760,72 +832,16 @@ static int fts5ExprNearNextMatch( Fts5Expr *pExpr, /* Expression that pNear is a part of */ Fts5ExprNode *pNode /* The "NEAR" node (FTS5_STRING) */ ){ - Fts5ExprNearset *pNear = pNode->pNear; int rc = SQLITE_OK; + assert( pNode->pNear ); while( 1 ){ - if( pNear->nPhrase==1 && pNear->apPhrase[0]->nTerm==1 ){ - /* If this "NEAR" object is actually a single phrase that consists - ** of a single term only, then grab pointers into the poslist - ** managed by the fts5_index.c iterator object. This is much faster - ** than synthesizing a new poslist the way we have to for more - ** complicated phrase or NEAR expressions. */ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[0]; - Fts5IndexIter *pIter = pPhrase->aTerm[0].pIter; - Fts5ExprColset *pColset = pNear->pColset; - const u8 *pPos; - int nPos; - - rc = sqlite3Fts5IterPoslist(pIter, &pPos, &nPos, &pNode->iRowid); - - /* If the term may match any column, then this must be a match. - ** Return immediately in this case. Otherwise, try to find the - ** part of the poslist that corresponds to the required column. - ** If it can be found, return. If it cannot, the next iteration - ** of the loop will test the next rowid in the database for this - ** term. */ - if( pColset==0 ){ - assert( pPhrase->poslist.nSpace==0 ); - pPhrase->poslist.p = (u8*)pPos; - pPhrase->poslist.n = nPos; - }else if( pColset->nCol==1 ){ - assert( pPhrase->poslist.nSpace==0 ); - pPhrase->poslist.n = fts5ExprExtractCol(&pPos, nPos, pColset->aiCol[0]); - pPhrase->poslist.p = (u8*)pPos; - }else if( rc==SQLITE_OK ){ - rc = fts5ExprExtractColset(pColset, pPos, nPos, &pPhrase->poslist); - } - - if( pPhrase->poslist.n ) return rc; - }else{ - int i; + /* Advance the iterators until they all point to the same rowid */ + rc = fts5ExprNearNextRowidMatch(pExpr, pNode); + if( rc!=SQLITE_OK || pNode->bEof ) break; - /* Advance the iterators until they all point to the same rowid */ - rc = fts5ExprNearNextRowidMatch(pExpr, pNode); - if( rc!=SQLITE_OK || pNode->bEof ) break; - - /* Check that each phrase in the nearset matches the current row. - ** Populate the pPhrase->poslist buffers at the same time. If any - ** phrase is not a match, break out of the loop early. */ - for(i=0; rc==SQLITE_OK && inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - if( pPhrase->nTerm>1 || pNear->pColset ){ - int bMatch = 0; - rc = fts5ExprPhraseIsMatch(pExpr, pNear->pColset, pPhrase, &bMatch); - if( bMatch==0 ) break; - }else{ - rc = sqlite3Fts5IterPoslistBuffer( - pPhrase->aTerm[0].pIter, &pPhrase->poslist - ); - } - } - - if( i==pNear->nPhrase ){ - if( i==1 ) break; - if( fts5ExprNearIsMatch(&rc, pNear) ) break; - } - } + if( fts5ExprNearTest(&rc, pExpr, pNode) ) break; /* If control flows to here, then the current rowid is not a match. ** Advance all term iterators in all phrases to the next rowid. */ @@ -942,10 +958,11 @@ static int fts5ExprNodeNext( }; case FTS5_AND: { - rc = fts5ExprNodeNext(pExpr, pNode->pLeft, bFromValid, iFrom); - if( rc==SQLITE_OK ){ - /* todo: update (iFrom/bFromValid) here */ - rc = fts5ExprNodeNext(pExpr, pNode->pRight, bFromValid, iFrom); + Fts5ExprNode *pLeft = pNode->pLeft; + rc = fts5ExprNodeNext(pExpr, pLeft, bFromValid, iFrom); + if( rc==SQLITE_OK && pLeft->bEof==0 ){ + assert( !bFromValid || fts5RowidCmp(pExpr, pLeft->iRowid, iFrom)>=0 ); + rc = fts5ExprNodeNext(pExpr, pNode->pRight, 1, pLeft->iRowid); } break; } @@ -994,6 +1011,67 @@ static int fts5ExprNodeNext( return rc; } +static void fts5ExprNodeZeroPoslist(Fts5ExprNode *pNode){ + if( pNode->eType==FTS5_STRING ){ + Fts5ExprNearset *pNear = pNode->pNear; + int i; + for(i=0; inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + pPhrase->poslist.n = 0; + } + }else{ + fts5ExprNodeZeroPoslist(pNode->pLeft); + fts5ExprNodeZeroPoslist(pNode->pRight); + } +} + +static int fts5ExprNodeTest( + int *pRc, + Fts5Expr *pExpr, + i64 iRowid, + Fts5ExprNode *pNode +){ + int bRes = 0; + if( pNode->bEof || pNode->iRowid!=iRowid ){ + bRes = 0; + }else { + switch( pNode->eType ){ + case FTS5_STRING: + bRes = fts5ExprNearTest(pRc, pExpr, pNode); + if( *pRc ) bRes = 0; + break; + + case FTS5_AND: { + int bRes1 = fts5ExprNodeTest(pRc, pExpr, iRowid, pNode->pLeft); + int bRes2 = fts5ExprNodeTest(pRc, pExpr, iRowid, pNode->pRight); + assert( (bRes1==0 || bRes1==1) && (bRes2==0 || bRes2==1) ); + + bRes = (bRes1 && bRes2); + if( bRes1!=bRes2 ){ + fts5ExprNodeZeroPoslist(bRes1 ? pNode->pLeft : pNode->pRight); + } + break; + } + + case FTS5_OR: { + int bRes1 = fts5ExprNodeTest(pRc, pExpr, iRowid, pNode->pLeft); + int bRes2 = fts5ExprNodeTest(pRc, pExpr, iRowid, pNode->pRight); + + bRes = (bRes1 || bRes2); + break; + } + + default: + assert( pNode->eType==FTS5_NOT ); + bRes = fts5ExprNodeTest(pRc, pExpr, iRowid, pNode->pLeft); + break; + } + } + + return bRes; +} + + static void fts5ExprSetEof(Fts5ExprNode *pNode){ if( pNode ){ pNode->bEof = 1; @@ -1016,7 +1094,10 @@ static int fts5ExprNodeNextMatch( switch( pNode->eType ){ case FTS5_STRING: { +#if 0 rc = fts5ExprNearNextMatch(pExpr, pNode); +#endif + rc = fts5ExprNearNextRowidMatch(pExpr, pNode); break; } @@ -1065,7 +1146,7 @@ static int fts5ExprNodeNextMatch( cmp = fts5NodeCompare(pExpr, p1, p2); } assert( rc!=SQLITE_OK || cmp<=0 ); - if( rc || cmp<0 ) break; + if( 0==fts5ExprNodeTest(&rc, pExpr, p1->iRowid, p2) ) break; rc = fts5ExprNodeNext(pExpr, p1, 0, 0); } pNode->bEof = p1->bEof; @@ -1096,7 +1177,10 @@ static int fts5ExprNodeFirst(Fts5Expr *pExpr, Fts5ExprNode *pNode){ /* Attempt to advance to the first match */ if( rc==SQLITE_OK && pNode->bEof==0 ){ +#if 0 rc = fts5ExprNearNextMatch(pExpr, pNode); +#endif + rc = fts5ExprNearNextRowidMatch(pExpr, pNode); } }else{ @@ -1112,7 +1196,6 @@ static int fts5ExprNodeFirst(Fts5Expr *pExpr, Fts5ExprNode *pNode){ } - /* ** Begin iterating through the set of documents in index pIdx matched by ** the MATCH expression passed as the first argument. If the "bDesc" parameter @@ -1123,11 +1206,18 @@ static int fts5ExprNodeFirst(Fts5Expr *pExpr, Fts5ExprNode *pNode){ ** is not considered an error if the query does not match any documents. */ int sqlite3Fts5ExprFirst(Fts5Expr *p, Fts5Index *pIdx, int bDesc){ + Fts5ExprNode *pRoot = p->pRoot; int rc = SQLITE_OK; - if( p->pRoot ){ + if( pRoot ){ p->pIndex = pIdx; p->bDesc = bDesc; - rc = fts5ExprNodeFirst(p, p->pRoot); + rc = fts5ExprNodeFirst(p, pRoot); + if( pRoot->bEof==0 + && 0==fts5ExprNodeTest(&rc, p, pRoot->iRowid, pRoot) + && rc==SQLITE_OK + ){ + rc = sqlite3Fts5ExprNext(p); + } } return rc; } @@ -1140,7 +1230,12 @@ int sqlite3Fts5ExprFirst(Fts5Expr *p, Fts5Index *pIdx, int bDesc){ */ int sqlite3Fts5ExprNext(Fts5Expr *p){ int rc; - rc = fts5ExprNodeNext(p, p->pRoot, 0, 0); + do { + rc = fts5ExprNodeNext(p, p->pRoot, 0, 0); + }while( p->pRoot->bEof==0 + && fts5ExprNodeTest(&rc, p, p->pRoot->iRowid, p->pRoot)==0 + && rc==SQLITE_OK + ); return rc; } diff --git a/ext/fts5/test/fts5_common.tcl b/ext/fts5/test/fts5_common.tcl index e4a689bb72..deffec5c4a 100644 --- a/ext/fts5/test/fts5_common.tcl +++ b/ext/fts5/test/fts5_common.tcl @@ -279,7 +279,7 @@ proc OR {a b} { sort_poslist [concat $a $b] } proc NOT {a b} { - if {[llength $b]} { return [list] } + if {[llength $b]>0} { return [list] } return $a } diff --git a/ext/fts5/test/fts5auto.test b/ext/fts5/test/fts5auto.test index 48d9148882..30333de221 100644 --- a/ext/fts5/test/fts5auto.test +++ b/ext/fts5/test/fts5auto.test @@ -226,32 +226,13 @@ set data { {k s} {r f e j q p w} } -do_test 1.0 { - execsql { - BEGIN; - CREATE VIRTUAL TABLE tt USING fts5(a, b, c, d, e, f); - } - foreach {rowid a b c d e f} $data { - execsql { - INSERT INTO tt(rowid, a, b, c, d, e, f) - VALUES($rowid, $a, $b, $c, $d, $e, $f) - } - } - execsql { - COMMIT; - } +do_execsql_test 1.0 { + CREATE VIRTUAL TABLE tt USING fts5(a, b, c, d, e, f); } {} -proc fts5_test_poslist {cmd} { - set res [list] - for {set i 0} {$i < [$cmd xInstCount]} {incr i} { - lappend res [string map {{ } .} [$cmd xInst $i]] - } - set res -} -sqlite3_fts5_create_function db fts5_test_poslist fts5_test_poslist +fts5_aux_test_functions db -proc matchdata {expr} { +proc matchdata {expr {order ASC}} { set tclexpr [db one { SELECT fts5_expr_tcl( $expr, 'nearset $cols -pc ::pc', 'a','b','c','d','e','f' @@ -259,49 +240,88 @@ proc matchdata {expr} { }] set res [list] - db eval {SELECT rowid, * FROM tt} { + db eval "SELECT rowid, * FROM tt ORDER BY rowid $order" { set cols [list $a $b $c $d $e $f] set ::pc 0 set rowdata [eval $tclexpr] - - if {$rowdata != ""} { - lappend res $rowid $rowdata - } + if {$rowdata != ""} { lappend res $rowid $rowdata } } set res } +proc do_auto_test {tn expr} { + foreach order {asc desc} { + set res [matchdata $expr $order] + set testname "3.$tn.[string range $order 0 0].rows=[expr [llength $res]/2]" + + set ::autotest_expr $expr + do_execsql_test $testname [subst -novar { + SELECT rowid, fts5_test_poslist(tt) FROM tt + WHERE tt MATCH $::autotest_expr ORDER BY rowid [set order] + }] $res + } + + +} + #------------------------------------------------------------------------- # -do_execsql_test 2.0 { - SELECT rowid, fts5_test_poslist(tt) FROM tt WHERE tt MATCH 'a AND b'; -} [matchdata "a AND b"] +for {set fold 0} {$fold < 3} {incr fold} { + switch $fold { + 0 { set map {} } + 1 { set map { + a a b a c b d b e c f c g d h d + i e j e k f l f m g g g o h p h + q i r i s j t j u k v k w l x l + y m z m + }} -do_test 2.1 { - llength [matchdata "a AND b"] -} 62 + 2 { set map { + a a b a c a d a e a f a g a h a + i b j b k b l b m b g b o b p b + q c r c s c t c u c v c w c x c + }} + } -foreach {tn expr} { - 1 { [a] : x } - 2 { [a b] : x } - 3 { [a b f] : x } - 4 { [f a b] : x } - 5 { [f a b] : x y } - 6 { [f a b] : x + y } - 7 { [c a b] : x + c } - 8 { [c d] : "l m" } - 9 { [c e] : "l m" } -} { - set res [matchdata $expr] - do_test 3.$tn.[llength $res] { + execsql { + BEGIN; + DELETE FROM tt; + } + foreach {rowid a b c d e f} [string map $map $data] { execsql { - SELECT rowid, fts5_test_poslist(tt) FROM tt WHERE tt MATCH $expr + INSERT INTO tt(rowid, a, b, c, d, e, f) + VALUES($rowid, $a, $b, $c, $d, $e, $f) } - } $res -} + } + execsql COMMIT + + foreach {tn expr} { + 3.1 { [a] : x } + 3.2 { [a b] : x } + 3.3 { [a b f] : x } + 3.4 { [f a b] : x } + 3.5 { [f a b] : x y } + 3.6 { [f a b] : x + y } + 3.7 { [c a b] : x + c } + 3.8 { [c d] : "l m" } + 3.9 { [c e] : "l m" } + + 4.1 { a NOT b } + 4.2 { a NOT a:b } + 4.3 { a OR (b AND c) } + 4.4 { a OR (b AND [a b c]:c) } + 4.5 { a OR "b c" } + 4.6 { a OR b OR c } + + 5.1 { a OR (b AND "b c") } + 5.2 { a OR (b AND "z c") } + } { + do_auto_test 3.$fold.$tn $expr + } +} finish_test diff --git a/manifest b/manifest index 2f6be39866..874c169da6 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Remove\sthe\s"#include\ssqlite3Int.h"\sfrom\sfts5Int.h. -D 2015-05-30T11:49:58.614 +C Change\sfts5\sexpression\sprocessing\sto\savoid\slinear\sscans\sof\slong\sdoclists\scaused\sby\sphrases\sthat\smatch\sspecific\scolumns\sonly. +D 2015-06-01T09:15:20.958 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f F Makefile.in 2c28e557780395095c307a6e5cb539419027eb5e F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 @@ -111,7 +111,7 @@ F ext/fts5/fts5Int.h 4c677f3b797acde90ba1b7730eca6a32e7def742 F ext/fts5/fts5_aux.c d53f00f31ad615ca4f139dd8751f9041afa00971 F ext/fts5/fts5_buffer.c 9ec57c75c81e81dca118568876b1caead0aadadf F ext/fts5/fts5_config.c 11f969ed711a0a8b611d47431d74c372ad78c713 -F ext/fts5/fts5_expr.c 6a683326d6ae4e58420792e84576af9c7a8a89e4 +F ext/fts5/fts5_expr.c e58c9dec148a92e9040abc613eb3c7506d741d4f F ext/fts5/fts5_hash.c c1cfdb2cae0fad00b06fae38a40eaf9261563ccc F ext/fts5/fts5_index.c 7cea402924cd3d8cd5943a7f9514c9153696571b F ext/fts5/fts5_storage.c 04e6717656b78eb230a1c730cac3b935eb94889b @@ -122,7 +122,7 @@ F ext/fts5/fts5_varint.c 366452037bf9a000c351374b489badc1b3541796 F ext/fts5/fts5_vocab.c 1f8543b2c1ae4427f127a911bc8e60873fcd7bf9 F ext/fts5/fts5parse.y 4ee667932d561a150d96483cf563281b95a9e523 F ext/fts5/mkportersteps.tcl 5acf962d2e0074f701620bb5308155fa1e4a63ba -F ext/fts5/test/fts5_common.tcl 632ff0fd8bf3dd55c2ddaac2c16428548d5af7be +F ext/fts5/test/fts5_common.tcl 339115b24a57244e792db465c5bad482e0e7db72 F ext/fts5/test/fts5aa.test 5f73afe6a1394fdba9bc18302876ded81021bee6 F ext/fts5/test/fts5ab.test 6fe3a56731d15978afbb74ae51b355fc9310f2ad F ext/fts5/test/fts5ac.test 999fd5f44579f1eb565ed7cf3861c427537ff097 @@ -135,7 +135,7 @@ F ext/fts5/test/fts5ai.test f20e53bbf0c55bc596f1fd47f2740dae028b8f37 F ext/fts5/test/fts5aj.test 05b569f5c16ea3098fb1984eec5cf50dbdaae5d8 F ext/fts5/test/fts5ak.test 7b8c5df96df599293f920b7e5521ebc79f647592 F ext/fts5/test/fts5al.test fc60ebeac9d8e366e71309d4c31fa72199d711d7 -F ext/fts5/test/fts5auto.test 62e62fa7d60c50d334c5f6cf6b1ed1d49fa3d8d8 +F ext/fts5/test/fts5auto.test 3810c1c4928be0161b87dfc479ecf1b873f37c6c F ext/fts5/test/fts5aux.test e5631607bbc05ac1c38cf7d691000509aca71ef3 F ext/fts5/test/fts5auxdata.test c69b86092bf1a157172de5f9169731af3403179b F ext/fts5/test/fts5bigpl.test b1cfd00561350ab04994ba7dd9d48468e5e0ec3b @@ -1333,7 +1333,7 @@ F tool/vdbe_profile.tcl 67746953071a9f8f2f668b73fe899074e2c6d8c1 F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4 F tool/warnings.sh 0abfd78ceb09b7f7c27c688c8e3fe93268a13b32 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P ae6794ffa23ef6191bd8834422abf322d978c11b -R bc0305687d74df992086e66a1770f40c +P e008c3c8e29c843ec945ddad54b9688bbf2bdb44 +R 99cd144645ddb0146b8edfaf69348c90 U dan -Z 008bbbc1e4c3598d73e809e2e8e489be +Z 9df575eada62ea84610594e2c9d9937b diff --git a/manifest.uuid b/manifest.uuid index 3d91049339..64f51bd23f 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -e008c3c8e29c843ec945ddad54b9688bbf2bdb44 \ No newline at end of file +ec69e09a55b4daf1c40aeaaf9ee95091fe86f5c0 \ No newline at end of file