When creating fts tables in an attached database, the backing tables
are created in database 'main'.  This change propagates the
appropriate database name to the routines which build sql statements.

Note that I propagate the database name and table name separately.  I
briefly considered just making the table name be "db.table", but it
didn't fit so well in the model used to store the table name and other
information, and having the db name passed separately seemed a bit
more transparent. (CVS 3631)

FossilOrigin-Name: 283385d20724f0144f38de89bd179715ee5e738b
This commit is contained in:
shess 2007-02-07 01:01:17 +00:00
parent 32ffdb7393
commit 3438ea3b9e
6 changed files with 240 additions and 37 deletions

View File

@ -847,25 +847,31 @@ static char *string_dup(const char *s){
}
/* Format a string, replacing each occurrence of the % character with
* zName. This may be more convenient than sqlite_mprintf()
* zDb.zName. This may be more convenient than sqlite_mprintf()
* when one string is used repeatedly in a format string.
* The caller must free() the returned string. */
static char *string_format(const char *zFormat, const char *zName){
static char *string_format(const char *zFormat,
const char *zDb, const char *zName){
const char *p;
size_t len = 0;
size_t nDb = strlen(zDb);
size_t nName = strlen(zName);
size_t nFullTableName = nDb+1+nName;
char *result;
char *r;
/* first compute length needed */
for(p = zFormat ; *p ; ++p){
len += (*p=='%' ? nName : 1);
len += (*p=='%' ? nFullTableName : 1);
}
len += 1; /* for null terminator */
r = result = malloc(len);
for(p = zFormat; *p; ++p){
if( *p=='%' ){
memcpy(r, zDb, nDb);
r += nDb;
*r++ = '.';
memcpy(r, zName, nName);
r += nName;
} else {
@ -877,8 +883,9 @@ static char *string_format(const char *zFormat, const char *zName){
return result;
}
static int sql_exec(sqlite3 *db, const char *zName, const char *zFormat){
char *zCommand = string_format(zFormat, zName);
static int sql_exec(sqlite3 *db, const char *zDb, const char *zName,
const char *zFormat){
char *zCommand = string_format(zFormat, zDb, zName);
int rc;
TRACE(("FTS1 sql: %s\n", zCommand));
rc = sqlite3_exec(db, zCommand, NULL, 0, NULL);
@ -886,9 +893,9 @@ static int sql_exec(sqlite3 *db, const char *zName, const char *zFormat){
return rc;
}
static int sql_prepare(sqlite3 *db, const char *zName, sqlite3_stmt **ppStmt,
const char *zFormat){
char *zCommand = string_format(zFormat, zName);
static int sql_prepare(sqlite3 *db, const char *zDb, const char *zName,
sqlite3_stmt **ppStmt, const char *zFormat){
char *zCommand = string_format(zFormat, zDb, zName);
int rc;
TRACE(("FTS1 prepare: %s\n", zCommand));
rc = sqlite3_prepare(db, zCommand, -1, ppStmt, NULL);
@ -1040,6 +1047,7 @@ static const char *const fulltext_zStatement[MAX_STMT] = {
struct fulltext_vtab {
sqlite3_vtab base; /* Base class used by SQLite core */
sqlite3 *db; /* The database connection */
const char *zDb; /* logical database name */
const char *zName; /* virtual table name */
int nColumn; /* number of columns in virtual table */
char **azColumn; /* column names. malloced */
@ -1139,7 +1147,7 @@ static int sql_get_statement(fulltext_vtab *v, fulltext_statement iStmt,
default:
zStmt = fulltext_zStatement[iStmt];
}
rc = sql_prepare(v->db, v->zName, &v->pFulltextStatements[iStmt],
rc = sql_prepare(v->db, v->zDb, v->zName, &v->pFulltextStatements[iStmt],
zStmt);
if( zStmt != fulltext_zStatement[iStmt]) free((void *) zStmt);
if( rc!=SQLITE_OK ) return rc;
@ -1742,6 +1750,7 @@ static int startsWith(const char *s, const char *t){
** and use by fulltextConnect and fulltextCreate.
*/
typedef struct TableSpec {
const char *zDb; /* Logical database name */
const char *zName; /* Name of the full-text index */
int nColumn; /* Number of columns to be indexed */
char **azColumn; /* Original names of columns to be indexed */
@ -1804,6 +1813,7 @@ static int parseSpec(TableSpec *pSpec, int argc, const char *const*argv,
/* Identify the column names and the tokenizer and delimiter arguments
** in the argv[][] array.
*/
pSpec->zDb = azArg[1];
pSpec->zName = azArg[2];
pSpec->nColumn = 0;
pSpec->azColumn = azArg;
@ -1904,6 +1914,7 @@ static int constructVtab(
memset(v, 0, sizeof(*v));
/* sqlite will initialize v->base */
v->db = db;
v->zDb = spec->zDb; /* Freed when azColumn is freed */
v->zName = spec->zName; /* Freed when azColumn is freed */
v->nColumn = spec->nColumn;
v->azContentColumn = spec->azContentColumn;
@ -2020,11 +2031,11 @@ static int fulltextCreate(sqlite3 *db, void *pAux,
append(&schema, "CREATE TABLE %_content(");
appendList(&schema, spec.nColumn, spec.azContentColumn);
append(&schema, ")");
rc = sql_exec(db, spec.zName, schema.s);
rc = sql_exec(db, spec.zDb, spec.zName, schema.s);
free(schema.s);
if( rc!=SQLITE_OK ) goto out;
rc = sql_exec(db, spec.zName,
rc = sql_exec(db, spec.zDb, spec.zName,
"create table %_term(term text, segment integer, doclist blob, "
"primary key(term, segment));");
if( rc!=SQLITE_OK ) goto out;
@ -2082,7 +2093,7 @@ static int fulltextDestroy(sqlite3_vtab *pVTab){
int rc;
TRACE(("FTS1 Destroy %p\n", pVTab));
rc = sql_exec(v->db, v->zName,
rc = sql_exec(v->db, v->zDb, v->zName,
"drop table if exists %_content;"
"drop table if exists %_term;"
);
@ -2839,7 +2850,7 @@ static int fulltextFilter(
zSql = sqlite3_mprintf("select rowid, * from %%_content %s",
idxNum==QUERY_GENERIC ? "" : "where rowid=?");
sqlite3_finalize(c->pStmt);
rc = sql_prepare(v->db, v->zName, &c->pStmt, zSql);
rc = sql_prepare(v->db, v->zDb, v->zName, &c->pStmt, zSql);
sqlite3_free(zSql);
if( rc!=SQLITE_OK ) return rc;

View File

@ -1353,25 +1353,31 @@ static char *string_dup(const char *s){
}
/* Format a string, replacing each occurrence of the % character with
* zName. This may be more convenient than sqlite_mprintf()
* zDb.zName. This may be more convenient than sqlite_mprintf()
* when one string is used repeatedly in a format string.
* The caller must free() the returned string. */
static char *string_format(const char *zFormat, const char *zName){
static char *string_format(const char *zFormat,
const char *zDb, const char *zName){
const char *p;
size_t len = 0;
size_t nDb = strlen(zDb);
size_t nName = strlen(zName);
size_t nFullTableName = nDb+1+nName;
char *result;
char *r;
/* first compute length needed */
for(p = zFormat ; *p ; ++p){
len += (*p=='%' ? nName : 1);
len += (*p=='%' ? nFullTableName : 1);
}
len += 1; /* for null terminator */
r = result = malloc(len);
for(p = zFormat; *p; ++p){
if( *p=='%' ){
memcpy(r, zDb, nDb);
r += nDb;
*r++ = '.';
memcpy(r, zName, nName);
r += nName;
} else {
@ -1383,8 +1389,9 @@ static char *string_format(const char *zFormat, const char *zName){
return result;
}
static int sql_exec(sqlite3 *db, const char *zName, const char *zFormat){
char *zCommand = string_format(zFormat, zName);
static int sql_exec(sqlite3 *db, const char *zDb, const char *zName,
const char *zFormat){
char *zCommand = string_format(zFormat, zDb, zName);
int rc;
TRACE(("FTS2 sql: %s\n", zCommand));
rc = sqlite3_exec(db, zCommand, NULL, 0, NULL);
@ -1392,9 +1399,9 @@ static int sql_exec(sqlite3 *db, const char *zName, const char *zFormat){
return rc;
}
static int sql_prepare(sqlite3 *db, const char *zName, sqlite3_stmt **ppStmt,
const char *zFormat){
char *zCommand = string_format(zFormat, zName);
static int sql_prepare(sqlite3 *db, const char *zDb, const char *zName,
sqlite3_stmt **ppStmt, const char *zFormat){
char *zCommand = string_format(zFormat, zDb, zName);
int rc;
TRACE(("FTS2 prepare: %s\n", zCommand));
rc = sqlite3_prepare(db, zCommand, -1, ppStmt, NULL);
@ -1543,6 +1550,7 @@ static const char *const fulltext_zStatement[MAX_STMT] = {
struct fulltext_vtab {
sqlite3_vtab base; /* Base class used by SQLite core */
sqlite3 *db; /* The database connection */
const char *zDb; /* logical database name */
const char *zName; /* virtual table name */
int nColumn; /* number of columns in virtual table */
char **azColumn; /* column names. malloced */
@ -1642,7 +1650,7 @@ static int sql_get_statement(fulltext_vtab *v, fulltext_statement iStmt,
default:
zStmt = fulltext_zStatement[iStmt];
}
rc = sql_prepare(v->db, v->zName, &v->pFulltextStatements[iStmt],
rc = sql_prepare(v->db, v->zDb, v->zName, &v->pFulltextStatements[iStmt],
zStmt);
if( zStmt != fulltext_zStatement[iStmt]) free((void *) zStmt);
if( rc!=SQLITE_OK ) return rc;
@ -1716,7 +1724,7 @@ static int sql_get_leaf_statement(fulltext_vtab *v, int idx,
sqlite3_stmt **ppStmt){
assert( idx>=0 && idx<MERGE_COUNT );
if( v->pLeafSelectStmts[idx]==NULL ){
int rc = sql_prepare(v->db, v->zName, &v->pLeafSelectStmts[idx],
int rc = sql_prepare(v->db, v->zDb, v->zName, &v->pLeafSelectStmts[idx],
LEAF_SELECT);
if( rc!=SQLITE_OK ) return rc;
}else{
@ -2334,6 +2342,7 @@ static int startsWith(const char *s, const char *t){
** and use by fulltextConnect and fulltextCreate.
*/
typedef struct TableSpec {
const char *zDb; /* Logical database name */
const char *zName; /* Name of the full-text index */
int nColumn; /* Number of columns to be indexed */
char **azColumn; /* Original names of columns to be indexed */
@ -2396,6 +2405,7 @@ static int parseSpec(TableSpec *pSpec, int argc, const char *const*argv,
/* Identify the column names and the tokenizer and delimiter arguments
** in the argv[][] array.
*/
pSpec->zDb = azArg[1];
pSpec->zName = azArg[2];
pSpec->nColumn = 0;
pSpec->azColumn = azArg;
@ -2496,6 +2506,7 @@ static int constructVtab(
CLEAR(v);
/* sqlite will initialize v->base */
v->db = db;
v->zDb = spec->zDb; /* Freed when azColumn is freed */
v->zName = spec->zName; /* Freed when azColumn is freed */
v->nColumn = spec->nColumn;
v->azContentColumn = spec->azContentColumn;
@ -2583,14 +2594,15 @@ static int fulltextCreate(sqlite3 *db, void *pAux,
append(&schema, "CREATE TABLE %_content(");
appendList(&schema, spec.nColumn, spec.azContentColumn);
append(&schema, ")");
rc = sql_exec(db, spec.zName, stringBufferData(&schema));
rc = sql_exec(db, spec.zDb, spec.zName, stringBufferData(&schema));
stringBufferDestroy(&schema);
if( rc!=SQLITE_OK ) goto out;
rc = sql_exec(db, spec.zName, "create table %_segments(block blob);");
rc = sql_exec(db, spec.zDb, spec.zName,
"create table %_segments(block blob);");
if( rc!=SQLITE_OK ) goto out;
rc = sql_exec(db, spec.zName,
rc = sql_exec(db, spec.zDb, spec.zName,
"create table %_segdir("
" level integer,"
" idx integer,"
@ -2655,7 +2667,7 @@ static int fulltextDestroy(sqlite3_vtab *pVTab){
int rc;
TRACE(("FTS2 Destroy %p\n", pVTab));
rc = sql_exec(v->db, v->zName,
rc = sql_exec(v->db, v->zDb, v->zName,
"drop table if exists %_content;"
"drop table if exists %_segments;"
"drop table if exists %_segdir;"
@ -3419,7 +3431,7 @@ static int fulltextFilter(
zSql = sqlite3_mprintf("select rowid, * from %%_content %s",
idxNum==QUERY_GENERIC ? "" : "where rowid=?");
sqlite3_finalize(c->pStmt);
rc = sql_prepare(v->db, v->zName, &c->pStmt, zSql);
rc = sql_prepare(v->db, v->zDb, v->zName, &c->pStmt, zSql);
sqlite3_free(zSql);
if( rc!=SQLITE_OK ) return rc;

View File

@ -1,5 +1,5 @@
C Additional\stests\sto\sgive\sfull\scoverage\stesting\sto\sticket\s#2211.\s(CVS\s3630)
D 2007-02-06T23:41:34
C http://www.sqlite.org/cvstrac/tktview?tn=2219\n\nWhen\screating\sfts\stables\sin\san\sattached\sdatabase,\sthe\sbacking\stables\nare\screated\sin\sdatabase\s'main'.\s\sThis\schange\spropagates\sthe\nappropriate\sdatabase\sname\sto\sthe\sroutines\swhich\sbuild\ssql\sstatements.\n\nNote\sthat\sI\spropagate\sthe\sdatabase\sname\sand\stable\sname\sseparately.\s\sI\nbriefly\sconsidered\sjust\smaking\sthe\stable\sname\sbe\s"db.table",\sbut\sit\ndidn't\sfit\sso\swell\sin\sthe\smodel\sused\sto\sstore\sthe\stable\sname\sand\sother\ninformation,\sand\shaving\sthe\sdb\sname\spassed\sseparately\sseemed\sa\sbit\nmore\stransparent.\s(CVS\s3631)
D 2007-02-07T01:01:17
F Makefile.in 7fa74bf4359aa899da5586e394d17735f221315f
F Makefile.linux-gcc 2d8574d1ba75f129aba2019f0b959db380a90935
F README 9c4e2d6706bdcc3efdd773ce752a8cdab4f90028
@ -21,7 +21,7 @@ F ext/README.txt 913a7bd3f4837ab14d7e063304181787658b14e1
F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e
F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b
F ext/fts1/ft_hash.h 1a35e654a235c2c662d3ca0dfc3138ad60b8b7d5
F ext/fts1/fts1.c 37b91a1a39713220c5411debe46778495fcfc2f2
F ext/fts1/fts1.c 0aab3cf20eefd38935c8f525494d689cb2785f1d
F ext/fts1/fts1.h 6060b8f62c1d925ea8356cb1a6598073eb9159a6
F ext/fts1/fts1_hash.c 3196cee866edbebb1c0521e21672e6d599965114
F ext/fts1/fts1_hash.h 957d378355ed29f672cd5add012ce8b088a5e089
@ -33,7 +33,7 @@ F ext/fts1/fulltext.h 08525a47852d1d62a0be81d3fc3fe2d23b094efd
F ext/fts1/simple_tokenizer.c 1844d72f7194c3fd3d7e4173053911bf0661b70d
F ext/fts1/tokenizer.h 0c53421b832366d20d720d21ea3e1f6e66a36ef9
F ext/fts2/README.txt 8c18f41574404623b76917b9da66fcb0ab38328d
F ext/fts2/fts2.c 5f7247b8eca4c83e95d66a8f6cadb0949bb356dc
F ext/fts2/fts2.c a49ed7292cbacbfcde6fdee1be4f6529277be3fa
F ext/fts2/fts2.h bbdab26d34f91974d5b9ade8b7836c140a7c4ce1
F ext/fts2/fts2_hash.c b3f22116d4ef0bc8f2da6e3fdc435c86d0951a9b
F ext/fts2/fts2_hash.h e283308156018329f042816eb09334df714e105e
@ -207,6 +207,7 @@ F test/fts1d.test a73deace5c18df4a549b12908bade4f05dcf1a2f
F test/fts1e.test 77244843e925560b5a0b70069c3e7ab62f181ed2
F test/fts1f.test cdab90834a7627a26fed2f75d2f451650fb31fad
F test/fts1i.test 6bfe08cdfdced063a39a50c8601da65e6274d879
F test/fts1j.test e4c0ffcd0ba2adce09c6b7b43ffd0749b5fda5c7
F test/fts1porter.test d86e9c3e0c7f8ff95add6582b4b585fb4e02b96d
F test/fts2a.test 103fc178d134c54c44c1938a4331e9e2030792d9
F test/fts2b.test 964abc0236c849c07ca1ae496bb25c268ae94816
@ -217,6 +218,7 @@ F test/fts2f.test b5f2dde48199d79e859f59d3d857c17dd62a0129
F test/fts2g.test c69a8ab43ec77d123976ba6cf9422d647ae63032
F test/fts2h.test 223af921323b409d4b5b18ff4e51619541b174bb
F test/fts2i.test 1b22451d1f13f7c509baec620dc3a4a754885dd6
F test/fts2j.test f68d7611f76309bc8b94170f3740d9fbbc061d9b
F test/func.test 71938e2ac704d8ce12f11810d475597640656ae9
F test/hook.test 7e7645fd9a033f79cce8fdff151e32715e7ec50a
F test/in.test 369cb2aa1eab02296b4ec470732fe8c131260b1d
@ -429,7 +431,7 @@ F www/tclsqlite.tcl bb0d1357328a42b1993d78573e587c6dcbc964b9
F www/vdbe.tcl 87a31ace769f20d3627a64fa1fade7fed47b90d0
F www/version3.tcl 890248cf7b70e60c383b0e84d77d5132b3ead42b
F www/whentouse.tcl 97e2b5cd296f7d8057e11f44427dea8a4c2db513
P 912faf18d86416b1a36660851f8a4554e6746875
R 7671dedbcdac0acf59ed5e145bb96e0d
U drh
Z cc828275335295a30c6cbbc23706d929
P ecb1f2fd7b96797459287eaab95693ae17ec2079
R 1584b834a23f13b852b265197c23db47
U shess
Z 3a97d1cfa72fc8326e11ebbeaf0b1200

View File

@ -1 +1 @@
ecb1f2fd7b96797459287eaab95693ae17ec2079
283385d20724f0144f38de89bd179715ee5e738b

89
test/fts1j.test Normal file
View File

@ -0,0 +1,89 @@
# 2007 February 6
#
# The author disclaims copyright to this source code.
#
#*************************************************************************
# This file implements regression tests for SQLite library. This
# tests creating fts1 tables in an attached database.
#
# $Id: fts1j.test,v 1.1 2007/02/07 01:01:18 shess Exp $
#
set testdir [file dirname $argv0]
source $testdir/tester.tcl
# If SQLITE_ENABLE_FTS1 is defined, omit this file.
ifcapable !fts1 {
finish_test
return
}
# Clean up anything left over from a previous pass.
file delete -force test2.db
file delete -force test2.db-journal
sqlite3 db2 test2.db
db eval {
CREATE VIRTUAL TABLE t3 USING fts1(content);
INSERT INTO t3 (rowid, content) VALUES(1, "hello world");
}
db2 eval {
CREATE VIRTUAL TABLE t1 USING fts1(content);
INSERT INTO t1 (rowid, content) VALUES(1, "hello world");
INSERT INTO t1 (rowid, content) VALUES(2, "hello there");
INSERT INTO t1 (rowid, content) VALUES(3, "cruel world");
}
# This has always worked because the t1_* tables used by fts1 will be
# the defaults.
do_test fts1j-1.1 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
SELECT rowid FROM t1 WHERE t1 MATCH 'hello';
DETACH DATABASE two;
}
} {1 2}
# Make certain we're detached if there was an error.
catch {db eval {DETACH DATABASE two}}
# In older code, this appears to work fine, but the t2_* tables used
# by fts1 will be created in database 'main' instead of database
# 'two'. It appears to work fine because the tables end up being the
# defaults, but obviously is badly broken if you hope to use things
# other than in the exact same ATTACH setup.
do_test fts1j-1.2 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
CREATE VIRTUAL TABLE two.t2 USING fts1(content);
INSERT INTO t2 (rowid, content) VALUES(1, "hello world");
INSERT INTO t2 (rowid, content) VALUES(2, "hello there");
INSERT INTO t2 (rowid, content) VALUES(3, "cruel world");
SELECT rowid FROM t2 WHERE t2 MATCH 'hello';
DETACH DATABASE two;
}
} {1 2}
catch {db eval {DETACH DATABASE two}}
# In older code, this broke because the fts1 code attempted to create
# t3_* tables in database 'main', but they already existed. Normally
# this wouldn't happen without t3 itself existing, in which case the
# fts1 code would never be called in the first place.
do_test fts1j-1.3 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
CREATE VIRTUAL TABLE two.t3 USING fts1(content);
INSERT INTO two.t3 (rowid, content) VALUES(2, "hello there");
INSERT INTO two.t3 (rowid, content) VALUES(3, "cruel world");
SELECT rowid FROM two.t3 WHERE t3 MATCH 'hello';
DETACH DATABASE two;
} db2
} {2}
catch {db eval {DETACH DATABASE two}}
catch {db2 close}
file delete -force test2.db
finish_test

89
test/fts2j.test Normal file
View File

@ -0,0 +1,89 @@
# 2007 February 6
#
# The author disclaims copyright to this source code.
#
#*************************************************************************
# This file implements regression tests for SQLite library. This
# tests creating fts2 tables in an attached database.
#
# $Id: fts2j.test,v 1.1 2007/02/07 01:01:18 shess Exp $
#
set testdir [file dirname $argv0]
source $testdir/tester.tcl
# If SQLITE_ENABLE_FTS2 is defined, omit this file.
ifcapable !fts2 {
finish_test
return
}
# Clean up anything left over from a previous pass.
file delete -force test2.db
file delete -force test2.db-journal
sqlite3 db2 test2.db
db eval {
CREATE VIRTUAL TABLE t3 USING fts2(content);
INSERT INTO t3 (rowid, content) VALUES(1, "hello world");
}
db2 eval {
CREATE VIRTUAL TABLE t1 USING fts2(content);
INSERT INTO t1 (rowid, content) VALUES(1, "hello world");
INSERT INTO t1 (rowid, content) VALUES(2, "hello there");
INSERT INTO t1 (rowid, content) VALUES(3, "cruel world");
}
# This has always worked because the t1_* tables used by fts2 will be
# the defaults.
do_test fts2j-1.1 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
SELECT rowid FROM t1 WHERE t1 MATCH 'hello';
DETACH DATABASE two;
}
} {1 2}
# Make certain we're detached if there was an error.
catch {db eval {DETACH DATABASE two}}
# In older code, this appears to work fine, but the t2_* tables used
# by fts2 will be created in database 'main' instead of database
# 'two'. It appears to work fine because the tables end up being the
# defaults, but obviously is badly broken if you hope to use things
# other than in the exact same ATTACH setup.
do_test fts2j-1.2 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
CREATE VIRTUAL TABLE two.t2 USING fts2(content);
INSERT INTO t2 (rowid, content) VALUES(1, "hello world");
INSERT INTO t2 (rowid, content) VALUES(2, "hello there");
INSERT INTO t2 (rowid, content) VALUES(3, "cruel world");
SELECT rowid FROM t2 WHERE t2 MATCH 'hello';
DETACH DATABASE two;
}
} {1 2}
catch {db eval {DETACH DATABASE two}}
# In older code, this broke because the fts2 code attempted to create
# t3_* tables in database 'main', but they already existed. Normally
# this wouldn't happen without t3 itself existing, in which case the
# fts2 code would never be called in the first place.
do_test fts2j-1.3 {
execsql {
ATTACH DATABASE 'test2.db' AS two;
CREATE VIRTUAL TABLE two.t3 USING fts2(content);
INSERT INTO two.t3 (rowid, content) VALUES(2, "hello there");
INSERT INTO two.t3 (rowid, content) VALUES(3, "cruel world");
SELECT rowid FROM two.t3 WHERE t3 MATCH 'hello';
DETACH DATABASE two;
} db2
} {2}
catch {db eval {DETACH DATABASE two}}
catch {db2 close}
file delete -force test2.db
finish_test