Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Further fixes and test cases related to external content tables. |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | fts5 |
Files: | files | file ages | folders |
SHA1: |
ce6a899baff7265a60c880098a9a57ea |
User & Date: | dan 2015-01-06 14:38:34.378 |
Context
2015-01-06
| ||
19:08 | Remove the iPos parameter from the tokenizer callback. Fix the "tokenchars" and "separators" options on the simple tokenizer. (check-in: 65f0262fb8 user: dan tags: fts5) | |
14:38 | Further fixes and test cases related to external content tables. (check-in: ce6a899baf user: dan tags: fts5) | |
2015-01-05
| ||
20:41 | Tests and fixes for fts5 external content tables. (check-in: 047aaf830d user: dan tags: fts5) | |
Changes
Changes to ext/fts5/fts5_config.c.
︙ | ︙ | |||
212 213 214 215 216 217 218 | ** This function returns a copy of the identifier enclosed in backtick ** quotes. */ static char *fts5EscapeName(int *pRc, const char *z){ char *pRet = 0; if( *pRc==SQLITE_OK ){ int n = strlen(z); | | > | 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | ** This function returns a copy of the identifier enclosed in backtick ** quotes. */ static char *fts5EscapeName(int *pRc, const char *z){ char *pRet = 0; if( *pRc==SQLITE_OK ){ int n = strlen(z); pRet = (char*)sqlite3_malloc(2 + 2*n + 1); if( pRet==0 ){ *pRc = SQLITE_NOMEM; }else{ int i; char *p = pRet; *p++ = '`'; for(i=0; i<n; i++){ if( z[i]=='`' ) *p++ = '`'; *p++ = z[i]; } *p++ = '`'; *p++ = '\0'; } |
︙ | ︙ |
Changes to ext/fts5/fts5_index.c.
︙ | ︙ | |||
402 403 404 405 406 407 408 409 410 411 412 413 414 415 | ** ** aFirst[1] contains the index in aSeg[] of the iterator that points to ** the smallest key overall. aFirst[0] is unused. */ struct Fts5MultiSegIter { int nSeg; /* Size of aSeg[] array */ int bRev; /* True to iterate in reverse order */ Fts5SegIter *aSeg; /* Array of segment iterators */ u16 *aFirst; /* Current merge state (see above) */ }; /* ** Object for iterating through a single segment, visiting each term/docid ** pair in the segment. | > | 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 | ** ** aFirst[1] contains the index in aSeg[] of the iterator that points to ** the smallest key overall. aFirst[0] is unused. */ struct Fts5MultiSegIter { int nSeg; /* Size of aSeg[] array */ int bRev; /* True to iterate in reverse order */ int bSkipEmpty; /* True to skip deleted entries */ Fts5SegIter *aSeg; /* Array of segment iterators */ u16 *aFirst; /* Current merge state (see above) */ }; /* ** Object for iterating through a single segment, visiting each term/docid ** pair in the segment. |
︙ | ︙ | |||
1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 | } } if( pIter->pLeaf ){ fts5SegIterReverseInitPage(p, pIter); } } /* ** Advance iterator pIter to the next entry. ** ** If an error occurs, Fts5Index.rc is set to an appropriate error code. It ** is not considered an error if the iterator reaches EOF. If an error has ** already occurred when this function is called, it is a no-op. | > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 | } } if( pIter->pLeaf ){ fts5SegIterReverseInitPage(p, pIter); } } /* ** Return true if the iterator passed as the second argument currently ** points to a delete marker. A delete marker is an entry with a 0 byte ** position-list. */ static int fts5SegIterIsDelete( Fts5Index *p, /* FTS5 backend object */ Fts5SegIter *pIter /* Iterator to advance */ ){ int bRet = 0; Fts5Data *pLeaf = pIter->pLeaf; if( p->rc==SQLITE_OK && pLeaf ){ if( pIter->iLeafOffset<pLeaf->n ){ bRet = (pLeaf->p[pIter->iLeafOffset]==0x00); }else{ Fts5Data *pNew = fts5DataRead(p, FTS5_SEGMENT_ROWID( pIter->iIdx, pIter->pSeg->iSegid, 0, pIter->iLeafPgno )); if( pNew ){ bRet = (pNew->p[4]==0x00); fts5DataRelease(pNew); } } } return bRet; } /* ** Advance iterator pIter to the next entry. ** ** If an error occurs, Fts5Index.rc is set to an appropriate error code. It ** is not considered an error if the iterator reaches EOF. If an error has ** already occurred when this function is called, it is a no-op. |
︙ | ︙ | |||
2090 2091 2092 2093 2094 2095 2096 | static void fts5MultiIterNext( Fts5Index *p, Fts5MultiSegIter *pIter, int bFrom, /* True if argument iFrom is valid */ i64 iFrom /* Advance at least as far as this */ ){ if( p->rc==SQLITE_OK ){ | > > | | | | | | | | > > > > > | 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 | static void fts5MultiIterNext( Fts5Index *p, Fts5MultiSegIter *pIter, int bFrom, /* True if argument iFrom is valid */ i64 iFrom /* Advance at least as far as this */ ){ if( p->rc==SQLITE_OK ){ int bUseFrom = bFrom; do { int iFirst = pIter->aFirst[1]; Fts5SegIter *pSeg = &pIter->aSeg[iFirst]; if( bUseFrom && pSeg->pDlidx ){ fts5SegIterNextFrom(p, pSeg, iFrom); }else{ fts5SegIterNext(p, pSeg); } fts5MultiIterAdvanced(p, pIter, iFirst, 1); bUseFrom = 0; }while( pIter->bSkipEmpty && fts5SegIterIsDelete(p, &pIter->aSeg[pIter->aFirst[1]]) ); } } /* ** Allocate a new Fts5MultiSegIter object. ** ** The new object will be used to iterate through data in structure pStruct. ** If iLevel is -ve, then all data in all segments is merged. Or, if iLevel ** is zero or greater, data from the first nSegment segments on level iLevel ** is merged. ** ** The iterator initially points to the first term/rowid entry in the ** iterated data. */ static void fts5MultiIterNew( Fts5Index *p, /* FTS5 backend to iterate within */ Fts5Structure *pStruct, /* Structure of specific index */ int iIdx, /* Config.aHash[] index of FTS index */ int bSkipEmpty, int flags, /* True for >= */ const u8 *pTerm, int nTerm, /* Term to seek to (or NULL/0) */ int iLevel, /* Level to iterate (-1 for all) */ int nSegment, /* Number of segments to merge (iLevel>=0) */ Fts5MultiSegIter **ppOut /* New object */ ){ int nSeg; /* Number of segments merged */ |
︙ | ︙ | |||
2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 | sizeof(u16) * nSlot /* pNew->aFirst[] */ ); if( pNew==0 ) return; pNew->nSeg = nSlot; pNew->aSeg = (Fts5SegIter*)&pNew[1]; pNew->aFirst = (u16*)&pNew->aSeg[nSlot]; pNew->bRev = (0!=(flags & FTS5INDEX_QUERY_ASC)); /* Initialize each of the component segment iterators. */ if( iLevel<0 ){ Fts5StructureLevel *pEnd = &pStruct->aLevel[pStruct->nLevel]; for(pLvl=&pStruct->aLevel[0]; pLvl<pEnd; pLvl++){ for(iSeg=pLvl->nSeg-1; iSeg>=0; iSeg--){ Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg]; | > | 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 | sizeof(u16) * nSlot /* pNew->aFirst[] */ ); if( pNew==0 ) return; pNew->nSeg = nSlot; pNew->aSeg = (Fts5SegIter*)&pNew[1]; pNew->aFirst = (u16*)&pNew->aSeg[nSlot]; pNew->bRev = (0!=(flags & FTS5INDEX_QUERY_ASC)); pNew->bSkipEmpty = bSkipEmpty; /* Initialize each of the component segment iterators. */ if( iLevel<0 ){ Fts5StructureLevel *pEnd = &pStruct->aLevel[pStruct->nLevel]; for(pLvl=&pStruct->aLevel[0]; pLvl<pEnd; pLvl++){ for(iSeg=pLvl->nSeg-1; iSeg>=0; iSeg--){ Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg]; |
︙ | ︙ | |||
2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 | for(iIter=nSlot-1; iIter>0; iIter--){ int iEq; if( (iEq = fts5MultiIterDoCompare(pNew, iIter)) ){ fts5SegIterNext(p, &pNew->aSeg[iEq]); fts5MultiIterAdvanced(p, pNew, iEq, iIter); } } }else{ fts5MultiIterFree(p, pNew); *ppOut = 0; } } /* | > > > > > > | 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 | for(iIter=nSlot-1; iIter>0; iIter--){ int iEq; if( (iEq = fts5MultiIterDoCompare(pNew, iIter)) ){ fts5SegIterNext(p, &pNew->aSeg[iEq]); fts5MultiIterAdvanced(p, pNew, iEq, iIter); } } if( pNew->bSkipEmpty && fts5SegIterIsDelete(p, &pNew->aSeg[pNew->aFirst[1]]) ){ fts5MultiIterNext(p, pNew, 0, 0); } }else{ fts5MultiIterFree(p, pNew); *ppOut = 0; } } /* |
︙ | ︙ | |||
2954 2955 2956 2957 2958 2959 2960 | bOldest = (pLvlOut->nSeg==1 && pStruct->nLevel==iLvl+2); #if 0 fprintf(stdout, "merging %d segments from level %d!", nInput, iLvl); fflush(stdout); #endif | | | 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 | bOldest = (pLvlOut->nSeg==1 && pStruct->nLevel==iLvl+2); #if 0 fprintf(stdout, "merging %d segments from level %d!", nInput, iLvl); fflush(stdout); #endif for(fts5MultiIterNew(p, pStruct, iIdx, 0, 0, 0, 0, iLvl, nInput, &pIter); fts5MultiIterEof(p, pIter)==0; fts5MultiIterNext(p, pIter, 0, 0) ){ Fts5SegIter *pSeg = &pIter->aSeg[ pIter->aFirst[1] ]; Fts5ChunkIter sPos; /* Used to iterate through position list */ /* If the segment being written is the oldest in the entire index and |
︙ | ︙ | |||
3685 3686 3687 3688 3689 3690 3691 | Fts5DoclistIter *pDoclist; int i; i64 iLastRowid = 0; Fts5MultiSegIter *p1 = 0; /* Iterator used to gather data from index */ Fts5Buffer doclist; memset(&doclist, 0, sizeof(doclist)); | | | 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 | Fts5DoclistIter *pDoclist; int i; i64 iLastRowid = 0; Fts5MultiSegIter *p1 = 0; /* Iterator used to gather data from index */ Fts5Buffer doclist; memset(&doclist, 0, sizeof(doclist)); for(fts5MultiIterNew(p, pStruct, 0, 1, 1, pToken, nToken, -1, 0, &p1); fts5MultiIterEof(p, p1)==0; fts5MultiIterNext(p, p1, 0, 0) ){ i64 iRowid = fts5MultiIterRowid(p1); int nTerm; const u8 *pTerm = fts5MultiIterTerm(p1, &nTerm); assert( memcmp(pToken, pTerm, MIN(nToken, nTerm))<=0 ); |
︙ | ︙ | |||
3766 3767 3768 3769 3770 3771 3772 | int rc; /* Return code */ u64 cksum2 = 0; /* Checksum based on contents of indexes */ /* Check that the checksum of the index matches the argument checksum */ for(iIdx=0; iIdx<=pConfig->nPrefix; iIdx++){ Fts5MultiSegIter *pIter; Fts5Structure *pStruct = fts5StructureRead(p, iIdx); | | | 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 | int rc; /* Return code */ u64 cksum2 = 0; /* Checksum based on contents of indexes */ /* Check that the checksum of the index matches the argument checksum */ for(iIdx=0; iIdx<=pConfig->nPrefix; iIdx++){ Fts5MultiSegIter *pIter; Fts5Structure *pStruct = fts5StructureRead(p, iIdx); for(fts5MultiIterNew(p, pStruct, iIdx, 0, 0, 0, 0, -1, 0, &pIter); fts5MultiIterEof(p, pIter)==0; fts5MultiIterNext(p, pIter, 0, 0) ){ Fts5PosIter sPos; /* Used to iterate through position list */ int n; /* Size of term in bytes */ i64 iRowid = fts5MultiIterRowid(pIter); char *z = (char*)fts5MultiIterTerm(pIter, &n); |
︙ | ︙ | |||
4027 4028 4029 4030 4031 4032 4033 | memset(pRet, 0, sizeof(Fts5IndexIter)); pRet->pIndex = p; if( iIdx>=0 ){ pRet->pStruct = fts5StructureRead(p, iIdx); if( pRet->pStruct ){ fts5MultiIterNew(p, pRet->pStruct, | | | 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 | memset(pRet, 0, sizeof(Fts5IndexIter)); pRet->pIndex = p; if( iIdx>=0 ){ pRet->pStruct = fts5StructureRead(p, iIdx); if( pRet->pStruct ){ fts5MultiIterNew(p, pRet->pStruct, iIdx, 1, flags, (const u8*)pToken, nToken, -1, 0, &pRet->pMulti ); } }else{ int bAsc = (flags & FTS5INDEX_QUERY_ASC)!=0; fts5SetupPrefixIter(p, bAsc, (const u8*)pToken, nToken, pRet); } } |
︙ | ︙ |
Changes to ext/fts5/fts5_storage.c.
︙ | ︙ | |||
58 59 60 61 62 63 64 | char **pzErrMsg /* OUT: Error message (if any) */ ){ int rc = SQLITE_OK; assert( eStmt>=0 && eStmt<ArraySize(p->aStmt) ); if( p->aStmt[eStmt]==0 ){ const char *azStmt[] = { | | | < < < | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | char **pzErrMsg /* OUT: Error message (if any) */ ){ int rc = SQLITE_OK; assert( eStmt>=0 && eStmt<ArraySize(p->aStmt) ); if( p->aStmt[eStmt]==0 ){ const char *azStmt[] = { "SELECT * FROM %s ORDER BY %s ASC", /* SCAN_ASC */ "SELECT * FROM %s ORDER BY %s DESC", /* SCAN_DESC */ "SELECT * FROM %s WHERE %s=?", /* LOOKUP */ "INSERT INTO %Q.'%q_content' VALUES(%s)", /* INSERT_CONTENT */ "REPLACE INTO %Q.'%q_content' VALUES(%s)", /* REPLACE_CONTENT */ "DELETE FROM %Q.'%q_content' WHERE id=?", /* DELETE_CONTENT */ "REPLACE INTO %Q.'%q_docsize' VALUES(?,?)", /* REPLACE_DOCSIZE */ "DELETE FROM %Q.'%q_docsize' WHERE id=?", /* DELETE_DOCSIZE */ "SELECT sz FROM %Q.'%q_docsize' WHERE id=?", /* LOOKUP_DOCSIZE */ "REPLACE INTO %Q.'%q_config' VALUES(?,?)", /* REPLACE_CONFIG */ }; Fts5Config *pC = p->pConfig; char *zSql = 0; switch( eStmt ){ case FTS5_STMT_SCAN_ASC: case FTS5_STMT_SCAN_DESC: case FTS5_STMT_LOOKUP: zSql = sqlite3_mprintf(azStmt[eStmt], pC->zContent, pC->zContentRowid); break; case FTS5_STMT_INSERT_CONTENT: case FTS5_STMT_REPLACE_CONTENT: { int nCol = pC->nCol + 1; |
︙ | ︙ | |||
721 722 723 724 725 726 727 | for(i=0; rc==SQLITE_OK && i<pConfig->nCol; i++){ if( p->aTotalSize[i]!=aTotalSize[i] ) rc = SQLITE_CORRUPT_VTAB; } } /* Check that the %_docsize and %_content tables contain the expected ** number of rows. */ | | | 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 | for(i=0; rc==SQLITE_OK && i<pConfig->nCol; i++){ if( p->aTotalSize[i]!=aTotalSize[i] ) rc = SQLITE_CORRUPT_VTAB; } } /* Check that the %_docsize and %_content tables contain the expected ** number of rows. */ if( rc==SQLITE_OK && pConfig->eContent==FTS5_CONTENT_NORMAL ){ i64 nRow; rc = fts5StorageCount(p, "content", &nRow); if( rc==SQLITE_OK && nRow!=p->nTotalRow ) rc = SQLITE_CORRUPT_VTAB; } if( rc==SQLITE_OK ){ i64 nRow; rc = fts5StorageCount(p, "docsize", &nRow); |
︙ | ︙ |
Changes to ext/fts5/test/fts5aa.test.
︙ | ︙ | |||
298 299 300 301 302 303 304 305 306 307 | SELECT t2 FROM t2 WHERE t2 MATCH '*stuff' } {1 {unknown special query: stuff}} do_test 12.3 { set res [db one { SELECT t2 FROM t2 WHERE t2 MATCH '* reads ' }] string is integer $res } {1} finish_test | > > > > > > > > > > > > > > > > > > > > > | 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | SELECT t2 FROM t2 WHERE t2 MATCH '*stuff' } {1 {unknown special query: stuff}} do_test 12.3 { set res [db one { SELECT t2 FROM t2 WHERE t2 MATCH '* reads ' }] string is integer $res } {1} #------------------------------------------------------------------------- # reset_db do_execsql_test 13.1 { CREATE VIRTUAL TABLE t1 USING fts5(x); INSERT INTO t1(rowid, x) VALUES(1, 'o n e'), (2, 't w o'); } {} do_execsql_test 13.2 { SELECT rowid FROM t1 WHERE t1 MATCH 'o'; } {2 1} do_execsql_test 13.4 { DELETE FROM t1 WHERE rowid=2; } {} do_execsql_test 13.5 { SELECT rowid FROM t1 WHERE t1 MATCH 'o'; } {1} finish_test |
Changes to ext/fts5/test/fts5content.test.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | if {![info exists testdir]} { set testdir [file join [file dirname [info script]] .. .. .. test] } source $testdir/tester.tcl set testprefix fts5content do_execsql_test 1.1 { CREATE VIRTUAL TABLE f1 USING fts5(a, b, content=''); INSERT INTO f1(rowid, a, b) VALUES(1, 'one', 'o n e'); INSERT INTO f1(rowid, a, b) VALUES(2, 'two', 't w o'); INSERT INTO f1(rowid, a, b) VALUES(3, 'three', 't h r e e'); } | > > > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | if {![info exists testdir]} { set testdir [file join [file dirname [info script]] .. .. .. test] } source $testdir/tester.tcl set testprefix fts5content #------------------------------------------------------------------------- # Contentless tables # do_execsql_test 1.1 { CREATE VIRTUAL TABLE f1 USING fts5(a, b, content=''); INSERT INTO f1(rowid, a, b) VALUES(1, 'one', 'o n e'); INSERT INTO f1(rowid, a, b) VALUES(2, 'two', 't w o'); INSERT INTO f1(rowid, a, b) VALUES(3, 'three', 't h r e e'); } |
︙ | ︙ | |||
79 80 81 82 83 84 85 | UPDATE f1 SET a = 'a b c' WHERE rowid = 2; } {1 {cannot UPDATE contentless fts5 table: f1}} do_execsql_test 1.15 { INSERT INTO f1(f1, rowid, a, b) VALUES('delete', 2, 'two', 't w o'); } {} | < < < > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | UPDATE f1 SET a = 'a b c' WHERE rowid = 2; } {1 {cannot UPDATE contentless fts5 table: f1}} do_execsql_test 1.15 { INSERT INTO f1(f1, rowid, a, b) VALUES('delete', 2, 'two', 't w o'); } {} do_execsql_test 1.16 { SELECT rowid FROM f1 WHERE f1 MATCH 'o'; } {4 1} do_execsql_test 1.17 { SELECT rowid FROM f1; } {4 3 1} #------------------------------------------------------------------------- # External content tables # reset_db do_execsql_test 2.1 { -- Create a table. And an external content fts5 table to index it. CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c); CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a'); -- Triggers to keep the FTS index up to date. CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END; CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); END; CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END; } do_execsql_test 2.2 { INSERT INTO tbl VALUES(1, 'one', 'o n e'); INSERT INTO tbl VALUES(NULL, 'two', 't w o'); INSERT INTO tbl VALUES(3, 'three', 't h r e e'); } do_execsql_test 2.3 { INSERT INTO fts_idx(fts_idx) VALUES('integrity-check'); } do_execsql_test 2.4 { DELETE FROM tbl WHERE rowid=2; INSERT INTO fts_idx(fts_idx) VALUES('integrity-check'); } do_execsql_test 2.5 { UPDATE tbl SET c = c || ' x y z'; INSERT INTO fts_idx(fts_idx) VALUES('integrity-check'); } do_execsql_test 2.6 { SELECT * FROM fts_idx WHERE fts_idx MATCH 't AND x'; } {three {t h r e e x y z}} do_execsql_test 2.7 { SELECT highlight(fts_idx, 1, '[', ']') FROM fts_idx WHERE fts_idx MATCH 't AND x'; } {{[t] h r e e [x] y z}} finish_test |