Index: src/delete.c ================================================================== --- src/delete.c +++ src/delete.c @@ -500,11 +500,15 @@ sqlite3VdbeAddOp4Int(v, OP_NotFound, iDataCur, addrBypass, iKey, nKey); VdbeCoverage(v); } }else if( pPk ){ addrLoop = sqlite3VdbeAddOp1(v, OP_Rewind, iEphCur); VdbeCoverage(v); - sqlite3VdbeAddOp2(v, OP_RowData, iEphCur, iKey); + if( IsVirtual(pTab) ){ + sqlite3VdbeAddOp3(v, OP_Column, iEphCur, 0, iKey); + }else{ + sqlite3VdbeAddOp2(v, OP_RowData, iEphCur, iKey); + } assert( nKey==0 ); /* OP_Found will use a composite key */ }else{ addrLoop = sqlite3VdbeAddOp3(v, OP_RowSetRead, iRowSet, 0, iKey); VdbeCoverage(v); assert( nKey==1 ); Index: src/test_tclvar.c ================================================================== --- src/test_tclvar.c +++ src/test_tclvar.c @@ -13,10 +13,29 @@ ** is not included in the SQLite library. It is used for automated ** testing of the SQLite library. ** ** The emphasis of this file is a virtual table that provides ** access to TCL variables. +** +** The TCLVAR eponymous virtual table has a schema like this: +** +** CREATE TABLE tclvar( +** name TEXT, -- base name of the variable: "x" in "$x(y)" +** arrayname TEXT, -- array index name: "y" in "$x(y)" +** value TEXT, -- the value of the variable +** fullname TEXT, -- the full name of the variable +** PRIMARY KEY(fullname) +** ) WITHOUT ROWID; +** +** DELETE, INSERT, and UPDATE operations use the "fullname" field to +** determine the variable to be modified. Changing "value" to NULL +** deletes the variable. +** +** For SELECT operations, the "name" and "arrayname" fields will always +** match the "fullname" field. For DELETE, INSERT, and UPDATE, the +** "name" and "arrayname" fields are ignored and the variable is modified +** according to "fullname" and "value" only. */ #include "sqliteInt.h" #if defined(INCLUDE_SQLITE_TCL_H) # include "sqlite_tcl.h" #else @@ -65,11 +84,16 @@ sqlite3_vtab **ppVtab, char **pzErr ){ tclvar_vtab *pVtab; static const char zSchema[] = - "CREATE TABLE whatever(name TEXT, arrayname TEXT, value TEXT)"; + "CREATE TABLE x(" + " name TEXT," /* Base name */ + " arrayname TEXT," /* Array index */ + " value TEXT," /* Value */ + " fullname TEXT PRIMARY KEY" /* base(index) name */ + ") WITHOUT ROWID"; pVtab = sqlite3MallocZero( sizeof(*pVtab) ); if( pVtab==0 ) return SQLITE_NOMEM; *ppVtab = &pVtab->base; pVtab->interp = (Tcl_Interp *)pAux; sqlite3_declare_vtab(db, zSchema); @@ -249,10 +273,20 @@ case 2: { Tcl_Obj *pVal = Tcl_GetVar2Ex(interp, z1, *z2?z2:0, TCL_GLOBAL_ONLY); sqlite3_result_text(ctx, Tcl_GetString(pVal), -1, SQLITE_TRANSIENT); break; } + case 3: { + char *z3; + if( p2 ){ + z3 = sqlite3_mprintf("%s(%s)", z1, z2); + sqlite3_result_text(ctx, z3, -1, sqlite3_free); + }else{ + sqlite3_result_text(ctx, z1, -1, SQLITE_TRANSIENT); + } + break; + } } return SQLITE_OK; } static int tclvarRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ @@ -373,10 +407,62 @@ pIdxInfo->idxStr = zStr; pIdxInfo->needToFreeIdxStr = 1; return SQLITE_OK; } + +/* +** Invoked for any UPDATE, INSERT, or DELETE against a tclvar table +*/ +static int tclvarUpdate( + sqlite3_vtab *tab, + int argc, + sqlite3_value **argv, + sqlite_int64 *pRowid +){ + tclvar_vtab *pTab = (tclvar_vtab*)tab; + if( argc==1 ){ + /* A DELETE operation. The variable to be deleted is stored in argv[0] */ + const char *zVar = (const char*)sqlite3_value_text(argv[0]); + Tcl_UnsetVar(pTab->interp, zVar, TCL_GLOBAL_ONLY); + return SQLITE_OK; + } + if( sqlite3_value_type(argv[0])==SQLITE_NULL ){ + /* An INSERT operation */ + const char *zValue = (const char*)sqlite3_value_text(argv[4]); + const char *zName; + if( sqlite3_value_type(argv[5])!=SQLITE_TEXT ){ + tab->zErrMsg = sqlite3_mprintf("the 'fullname' column must be TEXT"); + return SQLITE_ERROR; + } + zName = (const char*)sqlite3_value_text(argv[5]); + if( zValue ){ + Tcl_SetVar(pTab->interp, zName, zValue, TCL_GLOBAL_ONLY); + }else{ + Tcl_UnsetVar(pTab->interp, zName, TCL_GLOBAL_ONLY); + } + return SQLITE_OK; + } + if( sqlite3_value_type(argv[0])==SQLITE_TEXT + && sqlite3_value_type(argv[1])==SQLITE_TEXT + ){ + /* An UPDATE operation */ + const char *zOldName = (const char*)sqlite3_value_text(argv[0]); + const char *zNewName = (const char*)sqlite3_value_text(argv[1]); + const char *zValue = (const char*)sqlite3_value_text(argv[4]); + + if( strcmp(zOldName, zNewName)!=0 || zValue==0 ){ + Tcl_UnsetVar(pTab->interp, zOldName, TCL_GLOBAL_ONLY); + } + if( zValue!=0 ){ + Tcl_SetVar(pTab->interp, zNewName, zValue, TCL_GLOBAL_ONLY); + } + return SQLITE_OK; + } + tab->zErrMsg = sqlite3_mprintf("prohibited TCL variable change"); + return SQLITE_ERROR; +} /* ** A virtual table module that provides read-only access to a ** Tcl global variable namespace. */ @@ -392,11 +478,11 @@ tclvarFilter, /* xFilter - configure scan constraints */ tclvarNext, /* xNext - advance a cursor */ tclvarEof, /* xEof - check for end of scan */ tclvarColumn, /* xColumn - read data */ tclvarRowid, /* xRowid - read data */ - 0, /* xUpdate */ + tclvarUpdate, /* xUpdate */ 0, /* xBegin */ 0, /* xSync */ 0, /* xCommit */ 0, /* xRollback */ 0, /* xFindMethod */ Index: src/update.c ================================================================== --- src/update.c +++ src/update.c @@ -801,23 +801,34 @@ /* Start scanning the virtual table */ pWInfo = sqlite3WhereBegin(pParse, pSrc, pWhere, 0,0,WHERE_ONEPASS_DESIRED,0); if( pWInfo==0 ) return; /* Populate the argument registers. */ - sqlite3VdbeAddOp2(v, OP_Rowid, iCsr, regArg); - if( pRowid ){ - sqlite3ExprCode(pParse, pRowid, regArg+1); - }else{ - sqlite3VdbeAddOp2(v, OP_Rowid, iCsr, regArg+1); - } for(i=0; inCol; i++){ if( aXRef[i]>=0 ){ sqlite3ExprCode(pParse, pChanges->a[aXRef[i]].pExpr, regArg+2+i); }else{ sqlite3VdbeAddOp3(v, OP_VColumn, iCsr, i, regArg+2+i); } } + if( HasRowid(pTab) ){ + sqlite3VdbeAddOp2(v, OP_Rowid, iCsr, regArg); + if( pRowid ){ + sqlite3ExprCode(pParse, pRowid, regArg+1); + }else{ + sqlite3VdbeAddOp2(v, OP_Rowid, iCsr, regArg+1); + } + }else{ + Index *pPk; /* PRIMARY KEY index */ + i16 iPk; /* PRIMARY KEY column */ + pPk = sqlite3PrimaryKeyIndex(pTab); + assert( pPk!=0 ); + assert( pPk->nKeyCol==1 ); + iPk = pPk->aiColumn[0]; + sqlite3VdbeAddOp3(v, OP_VColumn, iCsr, iPk, regArg); + sqlite3VdbeAddOp2(v, OP_SCopy, regArg+2+iPk, regArg+1); + } bOnePass = sqlite3WhereOkOnePass(pWInfo, aDummy); if( bOnePass ){ /* If using the onepass strategy, no-op out the OP_OpenEphemeral coded Index: src/vtab.c ================================================================== --- src/vtab.c +++ src/vtab.c @@ -771,11 +771,17 @@ pTab->nCol = pNew->nCol; pTab->tabFlags |= pNew->tabFlags & (TF_WithoutRowid|TF_NoVisibleRowid); pNew->nCol = 0; pNew->aCol = 0; assert( pTab->pIndex==0 ); - if( !HasRowid(pNew) && pCtx->pVTable->pMod->pModule->xUpdate!=0 ){ + assert( HasRowid(pNew) || sqlite3PrimaryKeyIndex(pNew)!=0 ); + if( !HasRowid(pNew) + && pCtx->pVTable->pMod->pModule->xUpdate!=0 + && sqlite3PrimaryKeyIndex(pNew)->nKeyCol!=1 + ){ + /* WITHOUT ROWID virtual tables must either be read-only (xUpdate==0) + ** or else must have a single-column PRIMARY KEY */ rc = SQLITE_ERROR; } pIdx = pNew->pIndex; if( pIdx ){ assert( pIdx->pNext==0 ); Index: test/csv01.test ================================================================== --- test/csv01.test +++ test/csv01.test @@ -91,22 +91,54 @@ # The rowid column is not visible on a WITHOUT ROWID virtual table do_catchsql_test 3.2 { SELECT rowid, a FROM t3; } {1 {no such column: rowid}} +# Multi-column WITHOUT ROWID virtual tables may not be writable. do_catchsql_test 4.0 { DROP TABLE t3; CREATE VIRTUAL TABLE temp.t4 USING csv_wr( data= '1,2,3,4 5,6,7,8 9,10,11,12 -13,14,15,16 -', +13,14,15,16', columns=4, schema= - 'CREATE TABLE t3(a PRIMARY KEY,b TEXT,c TEXT,d TEXT) WITHOUT ROWID', + 'CREATE TABLE t3(a,b,c,d,PRIMARY KEY(a,b)) WITHOUT ROWID', testflags=1 ); } {1 {vtable constructor failed: t4}} + +# WITHOUT ROWID tables with a single-column PRIMARY KEY may be writable. +do_catchsql_test 4.1 { + DROP TABLE IF EXISTS t4; + CREATE VIRTUAL TABLE temp.t4 USING csv_wr( + data= +'1,2,3,4 +5,6,7,8 +9,10,11,12 +13,14,15,16', + columns=4, + schema= + 'CREATE TABLE t3(a,b,c,d,PRIMARY KEY(b)) WITHOUT ROWID', + testflags=1 + ); +} {0 {}} + +do_catchsql_test 4.2 { + DROP TABLE IF EXISTS t5; + CREATE VIRTUAL TABLE temp.t5 USING csv_wr( + data= + '1,2,3,4 + 5,6,7,8 + 9,10,11,12 + 13,14,15,16', + columns=4, + schema= + 'CREATE TABLE t3(a,b,c,d) WITHOUT ROWID', + testflags=1 + ); +} {1 {vtable constructor failed: t5}} + finish_test Index: test/vtab2.test ================================================================== --- test/vtab2.test +++ test/vtab2.test @@ -58,19 +58,19 @@ register_tclvar_module [sqlite3_connection_pointer db] do_test vtab2-2.1 { set ::abc 123 execsql { CREATE VIRTUAL TABLE vars USING tclvar; - SELECT * FROM vars WHERE name='abc'; + SELECT name, arrayname, value FROM vars WHERE name='abc'; } } [list abc "" 123] do_test vtab2-2.2 { set A(1) 1 set A(2) 4 set A(3) 9 execsql { - SELECT * FROM vars WHERE name='A'; + SELECT name, arrayname, value FROM vars WHERE name='A'; } } [list A 1 1 A 2 4 A 3 9] unset -nocomplain result unset -nocomplain var set result {} Index: test/vtabE.test ================================================================== --- test/vtabE.test +++ test/vtabE.test @@ -37,11 +37,13 @@ do_test vtabE-1 { db eval { CREATE VIRTUAL TABLE t1 USING tclvar; CREATE VIRTUAL TABLE t2 USING tclvar; CREATE TABLE t3(a INTEGER PRIMARY KEY, b); - SELECT t1.*, t2.*, abs(t3.b + abs(t2.value + abs(t1.value))) + SELECT t1.name, t1.arrayname, t1.value, + t2.name, t2.arrayname, t2.value, + abs(t3.b + abs(t2.value + abs(t1.value))) FROM t1 LEFT JOIN t2 ON t2.name = t1.arrayname LEFT JOIN t3 ON t3.a=t2.value WHERE t1.name = 'vtabE' ORDER BY t1.value, t2.value; } Index: test/vtabH.test ================================================================== --- test/vtabH.test +++ test/vtabH.test @@ -53,11 +53,11 @@ register_tclvar_module db set ::xyz 10 do_execsql_test 2.0 { CREATE VIRTUAL TABLE vars USING tclvar; - SELECT * FROM vars WHERE name = 'xyz'; + SELECT name, arrayname, value FROM vars WHERE name = 'xyz'; } {xyz {} 10} set x1 aback set x2 abaft set x3 abandon ADDED test/vtabJ.test Index: test/vtabJ.test ================================================================== --- /dev/null +++ test/vtabJ.test @@ -0,0 +1,126 @@ +# 2017-08-10 +# +# 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. +# +#*********************************************************************** +# This file implements tests of writing to WITHOUT ROWID virtual tables +# using the tclvar eponymous virtual table. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix vtabJ + +ifcapable !vtab { + finish_test + return +} + +register_tclvar_module db + +unset -nocomplain vtabJ +do_test 100 { + set vtabJ(1) this + set vtabJ(two) is + set vtabJ(3) {a test} + db eval { + SELECT fullname, value FROM tclvar WHERE name='vtabJ' ORDER BY fullname; + } +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(two) is} + +do_execsql_test 110 { + INSERT INTO tclvar(fullname, value) + VALUES('vtabJ(4)',4),('vtabJ(five)',555); + SELECT fullname, value FROM tclvar WHERE name='vtabJ' ORDER BY fullname; +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(4) 4 vtabJ(five) 555 vtabJ(two) is} +do_test 111 { + set res {} + foreach vname [lsort [array names vtabJ]] { + lappend res vtabJ($vname) $vtabJ($vname) + } + set res +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(4) 4 vtabJ(five) 555 vtabJ(two) is} + +do_test 120 { + db eval { + INSERT INTO tclvar(fullname, value) VALUES('vtabJ(4)',444); + } + set vtabJ(4) +} {444} + +do_test 130 { + db eval { + INSERT INTO tclvar(fullname, value) VALUES('vtabJ(4)',NULL); + } + info exists vtabJ(4) +} {0} + +do_test 140 { + db eval { + UPDATE tclvar SET value=55 WHERE fullname='vtabJ(five)'; + } + set vtabJ(five) +} {55} + +do_test 150 { + db eval { + UPDATE tclvar SET fullname='vtabJ(5)' WHERE fullname='vtabJ(five)'; + } + set vtabJ(5) +} {55} +do_test 151 { + info exists vtabJ(five) +} {0} +do_test 152 { + set res {} + foreach vname [lsort [array names vtabJ]] { + lappend res vtabJ($vname) $vtabJ($vname) + } + set res +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(5) 55 vtabJ(two) is} + +do_execsql_test 160 { + SELECT fullname FROM tclvar WHERE arrayname='two' +} {vtabJ(two)} +do_execsql_test 161 { + DELETE FROM tclvar WHERE arrayname='two'; + SELECT fullname, value FROM tclvar WHERE name='vtabJ' ORDER BY fullname; +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(5) 55} +do_test 162 { + set res {} + foreach vname [lsort [array names vtabJ]] { + lappend res vtabJ($vname) $vtabJ($vname) + } + set res +} {vtabJ(1) this vtabJ(3) {a test} vtabJ(5) 55} + +# Try to trick the module into updating the same variable twice for a +# single UPDATE statement. +# +do_execsql_test 171 { + INSERT INTO tclvar(fullname, value) VALUES('xx', 'a'); + SELECT name, value FROM tclvar where name = 'xx'; +} {xx a} +do_execsql_test 172 { + UPDATE tclvar SET value = value || 't' + WHERE name = 'xx' OR name = 'x'||'x'; + SELECT name, value FROM tclvar where name = 'xx'; +} {xx at} +do_execsql_test 173 { + UPDATE tclvar SET value = value || 't' + WHERE name = 'xx' OR name BETWEEN 'xx' AND 'xx'; + SELECT name, value FROM tclvar where name = 'xx'; +} {xx att} + +do_execsql_test 181 { + DELETE FROM tclvar WHERE name BETWEEN 'xx' AND 'xx' OR name='xx'; + SELECT name, value FROM tclvar where name = 'xx'; +} {} + + +finish_test