Index: src/alter.c ================================================================== --- src/alter.c +++ src/alter.c @@ -352,18 +352,18 @@ sqlite3VdbeAddOp4(v, OP_DropTable, iDb, 0, 0, pTab->zName, 0); /* Reload the table, index and permanent trigger schemas. */ zWhere = sqlite3MPrintf(pParse->db, "tbl_name=%Q", zName); if( !zWhere ) return; - sqlite3VdbeAddParseSchemaOp(v, iDb, zWhere); + sqlite3VdbeAddParseSchemaOp(pParse, iDb, zWhere); #ifndef SQLITE_OMIT_TRIGGER /* Now, if the table is not stored in the temp database, reload any temp ** triggers. Don't use IN(...) in case SQLITE_OMIT_SUBQUERY is defined. */ if( (zWhere=whereTempTriggers(pParse, pTab))!=0 ){ - sqlite3VdbeAddParseSchemaOp(v, 1, zWhere); + sqlite3VdbeAddParseSchemaOp(pParse, 1, zWhere); } #endif } /* Index: src/analyze.c ================================================================== --- src/analyze.c +++ src/analyze.c @@ -1312,10 +1312,11 @@ HashElem *k; int iStatCur; int iMem; int iTab; + sqlite3SchemaWritable(pParse, iDb); sqlite3BeginWriteOperation(pParse, 0, iDb); iStatCur = pParse->nTab; pParse->nTab += 3; openStatTable(pParse, iDb, iStatCur, 0, 0); iMem = pParse->nMem+1; Index: src/build.c ================================================================== --- src/build.c +++ src/build.c @@ -515,27 +515,23 @@ /* ** Reset the schema for the database at index iDb. Also reset the ** TEMP schema. */ void sqlite3ResetOneSchema(sqlite3 *db, int iDb){ - Db *pDb; - assert( iDbnDb ); - - /* Case 1: Reset the single schema identified by iDb */ - pDb = &db->aDb[iDb]; - assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); + /* If any database other than TEMP is reset, then also reset TEMP + ** since TEMP might be holding triggers that reference tables in the + ** other database. */ + Db *pDb = &db->aDb[1]; assert( pDb->pSchema!=0 ); sqlite3SchemaClear(pDb->pSchema); - /* If any database other than TEMP is reset, then also reset TEMP - ** since TEMP might be holding triggers that reference tables in the - ** other database. - */ + assert( iDbnDb ); + assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); + assert( db->aDb[iDb].pSchema!=0 ); + if( iDb!=1 ){ - pDb = &db->aDb[1]; - assert( pDb->pSchema!=0 ); - sqlite3SchemaClear(pDb->pSchema); + sqlite3SchemaUnuse(db, iDb); } return; } /* @@ -543,16 +539,15 @@ ** "main" and "temp") for a single database connection. */ void sqlite3ResetAllSchemasOfConnection(sqlite3 *db){ int i; sqlite3BtreeEnterAll(db); - for(i=0; inDb; i++){ - Db *pDb = &db->aDb[i]; - if( pDb->pSchema ){ - sqlite3SchemaClear(pDb->pSchema); - } + for(i=0; inDb; i = (i?i+1:2)){ + sqlite3SchemaUnuse(db, i); } + sqlite3SchemaClear(db->aDb[1].pSchema); + db->mDbFlags &= ~DBFLAG_SchemaChange; sqlite3VtabUnlockList(db); sqlite3BtreeLeaveAll(db); sqlite3CollapseDatabaseArray(db); } @@ -2036,11 +2031,11 @@ } } #endif /* Reparse everything to update our internal data structures */ - sqlite3VdbeAddParseSchemaOp(v, iDb, + sqlite3VdbeAddParseSchemaOp(pParse, iDb, sqlite3MPrintf(db, "tbl_name='%q' AND type!='trigger'", p->zName)); } /* Add the table to the in-memory representation of the database. @@ -2531,10 +2526,11 @@ if( noErr ) sqlite3CodeVerifyNamedSchema(pParse, pName->a[0].zDatabase); goto exit_drop_table; } iDb = sqlite3SchemaToIndex(db, pTab->pSchema); assert( iDb>=0 && iDbnDb ); + sqlite3SchemaWritable(pParse, iDb); /* If pTab is a virtual table, call ViewGetColumnNames() to ensure ** it is initialized. */ if( IsVirtual(pTab) && sqlite3ViewGetColumnNames(pParse, pTab) ){ @@ -3394,11 +3390,11 @@ ** to invalidate all pre-compiled statements. */ if( pTblName ){ sqlite3RefillIndex(pParse, pIndex, iMem); sqlite3ChangeCookie(pParse, iDb); - sqlite3VdbeAddParseSchemaOp(v, iDb, + sqlite3VdbeAddParseSchemaOp(pParse, iDb, sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName)); sqlite3VdbeAddOp0(v, OP_Expire); } sqlite3VdbeJumpHere(v, pIndex->tnum); @@ -3513,10 +3509,11 @@ sqlite3ErrorMsg(pParse, "index associated with UNIQUE " "or PRIMARY KEY constraint cannot be dropped", 0); goto exit_drop_index; } iDb = sqlite3SchemaToIndex(db, pIndex->pSchema); + sqlite3SchemaWritable(pParse, iDb); #ifndef SQLITE_OMIT_AUTHORIZATION { int code = SQLITE_DROP_INDEX; Table *pTab = pIndex->pTable; const char *zDb = db->aDb[iDb].zDbSName; Index: src/callback.c ================================================================== --- src/callback.c +++ src/callback.c @@ -458,18 +458,120 @@ if( pSchema->schemaFlags & DB_SchemaLoaded ){ pSchema->iGeneration++; pSchema->schemaFlags &= ~DB_SchemaLoaded; } } + +/* +** Global linked list of sharable Schema objects. Read and write access must +** be protected by the SQLITE_MUTEX_STATIC_MASTER mutex. +*/ +static Schema *SQLITE_WSD sharedSchemaList = 0; + +/* +** Check that the schema of db iDb is writable (either because it is the temp +** db schema or because the db handle was opened without +** SQLITE_OPEN_REUSE_SCHEMA). If so, do nothing. Otherwise, leave an +** error in the Parse object. +*/ +void sqlite3SchemaWritable(Parse *pParse, int iDb){ + if( iDb!=1 && (pParse->db->openFlags & SQLITE_OPEN_REUSE_SCHEMA) ){ + sqlite3ErrorMsg(pParse, "attempt to modify read-only schema"); + } +} + +/* +** Replace the Schema object currently associated with database iDb with +** an empty schema object, ready to be populated. If there are multiple +** users of the Schema, this means decrementing the current objects ref +** count and replacing it with a pointer to a new, empty, object. Or, if +** the database handle passed as the first argument of the schema object +** is the only user, remove it from the shared list (if applicable) and +** clear it in place. +*/ +void sqlite3SchemaUnuse(sqlite3 *db, int iDb){ + Db *pDb = &db->aDb[iDb]; + Schema *pSchema; + assert( iDb!=1 ); + if( (pSchema = pDb->pSchema) ){ + + if( (db->openFlags & SQLITE_OPEN_REUSE_SCHEMA) ){ + sqlite3_mutex_enter( sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER) ); + assert( pSchema->nRef>=1 ); + if( pSchema->nRef==1 ){ + Schema **pp; + for(pp=&sharedSchemaList; (*pp); pp=&(*pp)->pNext){ + if( *pp==pSchema ){ + *pp = pSchema->pNext; + break; + } + } + pSchema->pNext = 0; + }else{ + assert( db->openFlags & SQLITE_OPEN_REUSE_SCHEMA ); + pSchema->nRef--; + pDb->pSchema = pSchema = 0; + } + sqlite3_mutex_leave( sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER) ); + } + + if( pSchema==0 ){ + db->aDb[iDb].pSchema = sqlite3SchemaGet(db, 0); + }else{ + sqlite3SchemaClear(pSchema); + } + } +} + +/* +** The schema for database iDb of database handle db, which was opened +** with SQLITE_OPEN_REUSE_SCHEMA, has just been parsed. This function +** checks the global list (sharedSchemaList) for a matching schema and, +** if one is found, frees the newly parsed Schema object and adds a pointer +** to the existing shared schema in its place. Or, if there is no matching +** schema in the list, then the new schema is added to it. +*/ +void sqlite3SchemaReuse(sqlite3 *db, int iDb){ + Schema *pSchema = db->aDb[iDb].pSchema; + Schema *p; + assert( pSchema && iDb!=1 ); + + sqlite3_mutex_enter( sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER) ); + for(p=sharedSchemaList; p; p=p->pNext){ + if( p->cksum==pSchema->cksum + && p->schema_cookie==pSchema->schema_cookie + ){ + break; + } + } + if( !p ){ + /* No matching schema was found. */ + pSchema->pNext = sharedSchemaList; + sharedSchemaList = pSchema; + }else{ + /* Found a matching schema. Increase its ref count. */ + p->nRef++; + } + sqlite3_mutex_leave( sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER) ); + + /* If a matching schema was found in the shared schema list, free the + ** schema object just parsed, and add a pointer to the matching schema + ** to the db handle. */ + if( p ){ + sqlite3SchemaClear(pSchema); + sqlite3DbFree(0, pSchema); + db->aDb[iDb].pSchema = p; + } +} /* ** Find and return the schema associated with a BTree. Create ** a new one if necessary. */ Schema *sqlite3SchemaGet(sqlite3 *db, Btree *pBt){ Schema * p; - if( pBt ){ + if( pBt && (db->openFlags & SQLITE_OPEN_REUSE_SCHEMA)==0 ){ p = (Schema *)sqlite3BtreeSchema(pBt, sizeof(Schema), sqlite3SchemaClear); }else{ p = (Schema *)sqlite3DbMallocZero(0, sizeof(Schema)); } if( !p ){ @@ -478,8 +580,9 @@ sqlite3HashInit(&p->tblHash); sqlite3HashInit(&p->idxHash); sqlite3HashInit(&p->trigHash); sqlite3HashInit(&p->fkeyHash); p->enc = SQLITE_UTF8; + p->nRef = 1; } return p; } Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -1155,10 +1155,14 @@ struct Db *pDb = &db->aDb[j]; if( pDb->pBt ){ sqlite3BtreeClose(pDb->pBt); pDb->pBt = 0; if( j!=1 ){ + if( db->openFlags & SQLITE_OPEN_REUSE_SCHEMA ){ + sqlite3SchemaUnuse(db, j); + sqlite3DbFree(0, pDb->pSchema); + } pDb->pSchema = 0; } } } /* Clear the TEMP schema separately and last */ Index: src/prepare.c ================================================================== --- src/prepare.c +++ src/prepare.c @@ -33,10 +33,30 @@ sqlite3DbFree(db, *pData->pzErrMsg); *pData->pzErrMsg = z; } pData->rc = db->mallocFailed ? SQLITE_NOMEM_BKPT : SQLITE_CORRUPT_BKPT; } + +/* +** Update the Schema.cksum checksum to account for the database object +** specified by the three arguments following the first. +*/ +void schemaUpdateChecksum( + Schema *pSchema, /* Schema object being parsed */ + const char *zName, /* Name of new database object */ + const char *zRoot, /* Root page of new database object */ + const char *zSql /* SQL used to create new database object */ +){ + int i; + u64 cksum = pSchema->cksum; + if( zName ){ + for(i=0; zName[i]; i++) cksum += (cksum<<3) + zName[i]; + } + if( zRoot ) for(i=0; zRoot[i]; i++) cksum += (cksum<<3) + zRoot[i]; + if( zSql ) for(i=0; zSql[i]; i++) cksum += (cksum<<3) + zSql[i]; + pSchema->cksum = cksum; +} /* ** This is the callback routine for the code that initializes the ** database. See sqlite3Init() below for additional information. ** This routine is also called from the OP_ParseSchema opcode of the VDBE. @@ -119,10 +139,20 @@ /* Do Nothing */; }else if( sqlite3GetInt32(argv[1], &pIndex->tnum)==0 ){ corruptSchema(pData, argv[0], "invalid rootpage"); } } + + if( iDb!=1 && (db->openFlags & SQLITE_OPEN_REUSE_SCHEMA) ){ + /* If this schema might be used by multiple connections, ensure that + ** the affinity string is allocated here. Otherwise, there might be + ** a race condition where two threads attempt to allocate it + ** simultaneously. */ + Index *pIndex = sqlite3FindIndex(db, argv[0], db->aDb[iDb].zDbSName); + if( pIndex ) sqlite3IndexAffinityStr(db, pIndex); + schemaUpdateChecksum(db->aDb[iDb].pSchema, argv[0], argv[1], argv[2]); + } return 0; } /* ** Attempt to read the database schema and initialize internal @@ -322,10 +352,14 @@ ** even when its contents have been corrupted. */ DbSetProperty(db, iDb, DB_SchemaLoaded); rc = SQLITE_OK; } + + if( iDb!=1 && (db->openFlags & SQLITE_OPEN_REUSE_SCHEMA) ){ + sqlite3SchemaReuse(db, iDb); + } /* Jump here for an error that occurs after successfully allocating ** curMain and calling sqlite3BtreeEnter(). For an error that occurs ** before that point, jump to error_out. */ Index: src/sqlite.h.in ================================================================== --- src/sqlite.h.in +++ src/sqlite.h.in @@ -553,10 +553,12 @@ #define SQLITE_OPEN_SHAREDCACHE 0x00020000 /* Ok for sqlite3_open_v2() */ #define SQLITE_OPEN_PRIVATECACHE 0x00040000 /* Ok for sqlite3_open_v2() */ #define SQLITE_OPEN_WAL 0x00080000 /* VFS only */ /* Reserved: 0x00F00000 */ +#define SQLITE_OPEN_REUSE_SCHEMA 0x01000000 /* Ok for sqlite3_open_v2() */ + /* ** CAPI3REF: Device Characteristics ** ** The xDeviceCharacteristics method of the [sqlite3_io_methods] Index: src/sqliteInt.h ================================================================== --- src/sqliteInt.h +++ src/sqliteInt.h @@ -1187,10 +1187,14 @@ Table *pSeqTab; /* The sqlite_sequence table used by AUTOINCREMENT */ u8 file_format; /* Schema format version for this file */ u8 enc; /* Text encoding used by this database */ u16 schemaFlags; /* Flags associated with this schema */ int cache_size; /* Number of pages to use in the cache */ + + int nRef; /* Number of connections using this schema */ + u64 cksum; /* Checksum for this database schema */ + Schema *pNext; /* Next schema in shared schema list */ }; /* ** These macros can be used to test, set, or clear bits in the ** Db.pSchema->flags field. @@ -4083,10 +4087,13 @@ void sqlite3DeleteIndexSamples(sqlite3*,Index*); void sqlite3DefaultRowEst(Index*); void sqlite3RegisterLikeFunctions(sqlite3*, int); int sqlite3IsLikeFunction(sqlite3*,Expr*,int*,char*); void sqlite3SchemaClear(void *); +void sqlite3SchemaUnuse(sqlite3*, int); +void sqlite3SchemaReuse(sqlite3*, int); +void sqlite3SchemaWritable(Parse*, int); Schema *sqlite3SchemaGet(sqlite3 *, Btree *); int sqlite3SchemaToIndex(sqlite3 *db, Schema *); KeyInfo *sqlite3KeyInfoAlloc(sqlite3*,int,int); void sqlite3KeyInfoUnref(KeyInfo*); KeyInfo *sqlite3KeyInfoRef(KeyInfo*); Index: src/tclsqlite.c ================================================================== --- src/tclsqlite.c +++ src/tclsqlite.c @@ -3426,19 +3426,28 @@ if( b ){ flags |= SQLITE_OPEN_URI; }else{ flags &= ~SQLITE_OPEN_URI; } + }else if( strcmp(zArg, "-reuse-schema")==0 ){ + int b; + if( Tcl_GetBooleanFromObj(interp, objv[i+1], &b) ) return TCL_ERROR; + if( b ){ + flags |= SQLITE_OPEN_REUSE_SCHEMA; + }else{ + flags &= ~SQLITE_OPEN_REUSE_SCHEMA; + } }else{ Tcl_AppendResult(interp, "unknown option: ", zArg, (char*)0); return TCL_ERROR; } } if( objc<3 || (objc&1)!=1 ){ Tcl_WrongNumArgs(interp, 1, objv, "HANDLE FILENAME ?-vfs VFSNAME? ?-readonly BOOLEAN? ?-create BOOLEAN?" " ?-nomutex BOOLEAN? ?-fullmutex BOOLEAN? ?-uri BOOLEAN?" + " ?-reuse-schema BOOLEAN?" #if defined(SQLITE_HAS_CODEC) && !defined(SQLITE_OMIT_CODEC_FROM_TCL) " ?-key CODECKEY?" #endif ); return TCL_ERROR; Index: src/trigger.c ================================================================== --- src/trigger.c +++ src/trigger.c @@ -311,11 +311,11 @@ "INSERT INTO %Q.%s VALUES('trigger',%Q,%Q,0,'CREATE TRIGGER %q')", db->aDb[iDb].zDbSName, MASTER_NAME, zName, pTrig->table, z); sqlite3DbFree(db, z); sqlite3ChangeCookie(pParse, iDb); - sqlite3VdbeAddParseSchemaOp(v, iDb, + sqlite3VdbeAddParseSchemaOp(pParse, iDb, sqlite3MPrintf(db, "type='trigger' AND name='%q'", zName)); } if( db->init.busy ){ Trigger *pLink = pTrig; @@ -536,10 +536,11 @@ sqlite3 *db = pParse->db; int iDb; iDb = sqlite3SchemaToIndex(pParse->db, pTrigger->pSchema); assert( iDb>=0 && iDbnDb ); + sqlite3SchemaWritable(pParse, iDb); pTable = tableOfTrigger(pTrigger); assert( pTable ); assert( pTable->pSchema==pTrigger->pSchema || iDb==1 ); #ifndef SQLITE_OMIT_AUTHORIZATION { Index: src/vacuum.c ================================================================== --- src/vacuum.c +++ src/vacuum.c @@ -115,10 +115,11 @@ iDb = sqlite3FindDb(pParse->db, pNm); if( iDb<0 ) iDb = 0; #endif } if( iDb!=1 ){ + sqlite3SchemaWritable(pParse, iDb); sqlite3VdbeAddOp1(v, OP_Vacuum, iDb); sqlite3VdbeUsesBtree(v, iDb); } return; } Index: src/vdbe.h ================================================================== --- src/vdbe.h +++ src/vdbe.h @@ -195,11 +195,11 @@ #else # define sqlite3VdbeVerifyNoMallocRequired(A,B) # define sqlite3VdbeVerifyNoResultRow(A) #endif VdbeOp *sqlite3VdbeAddOpList(Vdbe*, int nOp, VdbeOpList const *aOp, int iLineno); -void sqlite3VdbeAddParseSchemaOp(Vdbe*,int,char*); +void sqlite3VdbeAddParseSchemaOp(Parse*,int,char*); void sqlite3VdbeChangeOpcode(Vdbe*, u32 addr, u8); void sqlite3VdbeChangeP1(Vdbe*, u32 addr, int P1); void sqlite3VdbeChangeP2(Vdbe*, u32 addr, int P2); void sqlite3VdbeChangeP3(Vdbe*, u32 addr, int P3); void sqlite3VdbeChangeP5(Vdbe*, u16 P5); Index: src/vdbeaux.c ================================================================== --- src/vdbeaux.c +++ src/vdbeaux.c @@ -307,12 +307,14 @@ ** as having been used. ** ** The zWhere string must have been obtained from sqlite3_malloc(). ** This routine will take ownership of the allocated memory. */ -void sqlite3VdbeAddParseSchemaOp(Vdbe *p, int iDb, char *zWhere){ +void sqlite3VdbeAddParseSchemaOp(Parse *pParse, int iDb, char *zWhere){ + Vdbe *p = pParse->pVdbe; int j; + sqlite3SchemaWritable(pParse, iDb); sqlite3VdbeAddOp4(p, OP_ParseSchema, iDb, 0, 0, zWhere, P4_DYNAMIC); for(j=0; jdb->nDb; j++) sqlite3VdbeUsesBtree(p, j); } /* Index: src/vtab.c ================================================================== --- src/vtab.c +++ src/vtab.c @@ -432,11 +432,11 @@ v = sqlite3GetVdbe(pParse); sqlite3ChangeCookie(pParse, iDb); sqlite3VdbeAddOp0(v, OP_Expire); zWhere = sqlite3MPrintf(db, "name='%q' AND type='table'", pTab->zName); - sqlite3VdbeAddParseSchemaOp(v, iDb, zWhere); + sqlite3VdbeAddParseSchemaOp(pParse, iDb, zWhere); iReg = ++pParse->nMem; sqlite3VdbeLoadString(v, iReg, pTab->zName); sqlite3VdbeAddOp2(v, OP_VCreate, iDb, iReg); } ADDED test/reuse1.test Index: test/reuse1.test ================================================================== --- /dev/null +++ test/reuse1.test @@ -0,0 +1,94 @@ +# 2017 August 9 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# + + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix reuse1 + + +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_execsql_test 1.0 { + CREATE TABLE t1(x INTEGER PRIMARY KEY, y UNIQUE, z); + CREATE INDEX i1 ON t1(z); + PRAGMA schema_version; +} {2} + +do_execsql_test -db db2 1.1 { + CREATE TABLE t1(x INTEGER PRIMARY KEY, y UNIQUE, z); + CREATE INDEX i1 ON t1(z); + PRAGMA schema_version; +} {2} + +do_test 1.2 { + db close + db2 close + sqlite3 db2 test.db2 -reuse-schema 1 + sqlite3 db test.db -reuse-schema 1 +} {} + +do_execsql_test -db db2 1.3.1 { + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); +} + +do_execsql_test 1.3.2 { + SELECT * FROM t1; + PRAGMA integrity_check; +} {ok} + +do_execsql_test -db db2 1.3.3 { + SELECT * FROM t1; + PRAGMA integrity_check; +} {1 2 3 4 5 6 ok} + +sqlite3 db3 test.db2 +do_execsql_test -db db3 1.4.1 { + ALTER TABLE t1 ADD COLUMN a; +} +do_execsql_test -db db2 1.4.2 { + SELECT * FROM t1; +} {1 2 3 {} 4 5 6 {}} +do_execsql_test 1.4.3 { + SELECT * FROM t1; +} {} + +db3 close +sqlite3 db3 test.db +do_execsql_test -db db3 1.5.0 { + CREATE TRIGGER tr1 AFTER INSERT ON t1 BEGIN + SELECT 1, 2, 3; + END; +} + +# Check that the schema cannot be modified if the db was opened with +# SQLITE_OPEN_REUSE_SCHEMA. +# +foreach {tn sql} { + 1 { CREATE TABLE t2(x, y) } + 2 { CREATE INDEX i2 ON t1(z) } + 3 { CREATE VIEW v2 AS SELECT * FROM t2 } + 4 { ALTER TABLE t1 RENAME TO t3 } + 5 { ALTER TABLE t1 ADD COLUMN xyz } + 6 { VACUUM } + 7 { DROP INDEX i1 } + 8 { DROP TABLE t1 } + 9 { DROP TRIGGER tr1 } + 10 { ANALYZE } +} { + do_catchsql_test 1.5.$tn $sql {1 {attempt to modify read-only schema}} +} + +finish_test Index: test/tclsqlite.test ================================================================== --- test/tclsqlite.test +++ test/tclsqlite.test @@ -20,11 +20,11 @@ set testdir [file dirname $argv0] source $testdir/tester.tcl # Check the error messages generated by tclsqlite # -set r "sqlite_orig HANDLE FILENAME ?-vfs VFSNAME? ?-readonly BOOLEAN? ?-create BOOLEAN? ?-nomutex BOOLEAN? ?-fullmutex BOOLEAN? ?-uri BOOLEAN?" +set r "sqlite_orig HANDLE FILENAME ?-vfs VFSNAME? ?-readonly BOOLEAN? ?-create BOOLEAN? ?-nomutex BOOLEAN? ?-fullmutex BOOLEAN? ?-uri BOOLEAN? ?-reuse-schema BOOLEAN?" if {[sqlite3 -has-codec]} { append r " ?-key CODECKEY?" } do_test tcl-1.1 { set v [catch {sqlite3 bogus} msg]