diff --git a/ext/session/session2.test b/ext/session/session2.test index 5411563f2e..357b806043 100644 --- a/ext/session/session2.test +++ b/ext/session/session2.test @@ -111,6 +111,75 @@ do_iterator_test 1.1 t1 { {INSERT t1 {} {f 1.5 f 99.9}} } +# Execute each of the following blocks of SQL on database [db1]. Collect +# changes using a session object. Apply the resulting changeset to +# database [db2]. Then check that the contents of the two databases are +# identical. +# + +set set_of_tests { + 1 { INSERT INTO %T1% VALUES(1, 2) } + + 2 { + INSERT INTO %T2% VALUES(1, NULL); + INSERT INTO %T2% VALUES(2, NULL); + INSERT INTO %T2% VALUES(3, NULL); + DELETE FROM %T2% WHERE a = 2; + INSERT INTO %T2% VALUES(4, NULL); + UPDATE %T2% SET b=0 WHERE b=1; + } + + 3 { INSERT INTO %T3% SELECT *, NULL FROM %T2% } + + 4 { + INSERT INTO %T3% SELECT a||a, b||b, NULL FROM %T3%; + DELETE FROM %T3% WHERE rowid%2; + } + + 5 { UPDATE %T3% SET c = a||b } + + 6 { UPDATE %T1% SET a = 32 } + + 7 { + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + DELETE FROM %T1% WHERE (rowid%3)==0; + } + + 8 { + BEGIN; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + ROLLBACK; + } + 9 { + BEGIN; + UPDATE %T1% SET b = 'xxx'; + ROLLBACK; + } + 10 { + BEGIN; + DELETE FROM %T1% WHERE 1; + ROLLBACK; + } + 11 { + INSERT INTO %T1% VALUES(randomblob(21000), randomblob(0)); + INSERT INTO %T1% VALUES(1.5, 1.5); + INSERT INTO %T1% VALUES(4.56, -99.999999999999999999999); + } + 12 { + INSERT INTO %T2% VALUES(NULL, NULL); + } +} + test_reset do_common_sql { CREATE TABLE t1(a PRIMARY KEY, b); @@ -118,65 +187,7 @@ do_common_sql { CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b)); } -# Execute each of the following blocks of SQL on database [db1]. Collect -# changes using a session object. Apply the resulting changeset to -# database [db2]. Then check that the contents of the two databases are -# identical. -# -foreach {tn sql} { - 1 { INSERT INTO t1 VALUES(1, 2) } - - 2 { - INSERT INTO t2 VALUES(1, NULL); - INSERT INTO t2 VALUES(2, NULL); - INSERT INTO t2 VALUES(3, NULL); - DELETE FROM t2 WHERE a = 2; - INSERT INTO t2 VALUES(4, NULL); - UPDATE t2 SET b=0 WHERE b=1; - } - - 3 { INSERT INTO t3 SELECT *, NULL FROM t2 } - - 4 { - INSERT INTO t3 SELECT a||a, b||b, NULL FROM t3; - DELETE FROM t3 WHERE rowid%2; - } - - 5 { UPDATE t3 SET c = a||b } - - 6 { UPDATE t1 SET a = 32 } - - 7 { - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 2 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 4 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 8 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 16 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 32 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 64 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 128 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 256 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 512 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 1024 - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; -- 2048 - DELETE FROM t1 WHERE (rowid%3)==0; - } - - 8 { - BEGIN; - INSERT INTO t1 SELECT randomblob(32), randomblob(32) FROM t1; - ROLLBACK; - } - 9 { - BEGIN; - UPDATE t1 SET b = 'xxx'; - ROLLBACK; - } - 10 { - BEGIN; - DELETE FROM t1 WHERE 1; - ROLLBACK; - } -} { +foreach {tn sql} [string map {%T1% t1 %T2% t2 %T3% t3} $set_of_tests] { do_then_apply_sql $sql do_test 1.$tn { compare_db db db2 } {} } @@ -209,73 +220,61 @@ do_test 2.0 { proc xTrace {args} { puts $args } -foreach {tn sql} { - - 1 { INSERT INTO aux.t1 VALUES(1, 2) } - - 2 { - INSERT INTO aux.t2 VALUES(1, NULL); - INSERT INTO aux.t2 VALUES(2, NULL); - INSERT INTO aux.t2 VALUES(3, NULL); - DELETE FROM aux.t2 WHERE a = 2; - INSERT INTO aux.t2 VALUES(4, NULL); - UPDATE aux.t2 SET b=0 WHERE b=1; - } - - 3 { INSERT INTO aux.t3 SELECT *, NULL FROM aux.t2 } - - 4 { - INSERT INTO aux.t3 SELECT a||a, b||b, NULL FROM aux.t3; - DELETE FROM aux.t3 WHERE rowid%2; - } - - 5 { UPDATE aux.t3 SET c = a||b } - - 6 { UPDATE aux.t1 SET a = 32 } - - 7 { - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - DELETE FROM aux.t1 WHERE (rowid%3)==0; - } - - 8 { - BEGIN; - INSERT INTO aux.t1 SELECT randomblob(32), randomblob(32) FROM aux.t1; - ROLLBACK; - } - 9 { - BEGIN; - UPDATE aux.t1 SET b = 'xxx'; - ROLLBACK; - } - 10 { - BEGIN; - DELETE FROM aux.t1 WHERE 1; - ROLLBACK; - } - 11 { - INSERT INTO aux.t1 VALUES(randomblob(21000), randomblob(0)); - INSERT INTO aux.t1 VALUES(1.5, 1.5); - INSERT INTO aux.t1 VALUES(4.56, -99.999999999999999999999); - } - -} { +foreach {tn sql} [ + string map {%T1% aux.t1 %T2% aux.t2 %T3% aux.t3} $set_of_tests +] { do_then_apply_sql $sql aux do_test 2.$tn { compare_db db3 db2 } {} } - - catch {db3 close} + +#------------------------------------------------------------------------- +# The following tests verify that NULL values in primary key columns are +# handled correctly by the session module. +# +test_reset +do_execsql_test 3.0 { + CREATE TABLE t1(a PRIMARY KEY); + CREATE TABLE t2(a, b, c, PRIMARY KEY(c, b)); + CREATE TABLE t3(a, b INTEGER PRIMARY KEY); +} + +foreach {tn sql changeset} { + 1 { + INSERT INTO t1 VALUES(123); + INSERT INTO t1 VALUES(NULL); + INSERT INTO t1 VALUES(456); + } { + {INSERT t1 {} {i 456}} + {INSERT t1 {} {i 123}} + } + + 2 { + UPDATE t1 SET a = NULL; + } { + {DELETE t1 {i 456} {}} + {DELETE t1 {i 123} {}} + } + + 3 { DELETE FROM t1 } { } + + 4 { + INSERT INTO t3 VALUES(NULL, NULL) + } { + {INSERT t3 {} {n {} i 1}} + } + + 5 { INSERT INTO t2 VALUES(1, 2, NULL) } { } + 6 { INSERT INTO t2 VALUES(1, NULL, 3) } { } + 7 { INSERT INTO t2 VALUES(1, NULL, NULL) } { } + 8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 {} {i 1 i 2 i 3}} } + 9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 {i 1 i 2 i 3} {}} } + +} { + do_iterator_test 3.$tn {t1 t2 t3} $sql $changeset +} + + finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 676ea3b44c..4663938bb2 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -286,11 +286,13 @@ static unsigned int sessionPreupdateHash( sqlite3 *db, /* Database handle */ SessionTable *pTab, /* Session table handle */ int bNew, /* True to hash the new.* PK */ - int *piHash /* OUT: Hash value */ + int *piHash, /* OUT: Hash value */ + int *pbNullPK ){ unsigned int h = 0; /* Hash value to return */ int i; /* Used to iterate through columns */ + assert( *pbNullPK==0 ); assert( pTab->nCol==sqlite3_preupdate_count(db) ); for(i=0; inCol; i++){ if( pTab->abPK[i] ){ @@ -329,6 +331,11 @@ static unsigned int sessionPreupdateHash( h = sessionHashAppendBlob(h, n, z); break; } + + default: + assert( eType==SQLITE_NULL ); + *pbNullPK = 1; + return SQLITE_OK; } } } @@ -357,27 +364,22 @@ static unsigned int sessionChangeHash( int eType = *a++; int isPK = pTab->abPK[i]; + /* It is not possible for eType to be SQLITE_NULL here. The session + ** module does not record changes for rows with NULL values stored in + ** primary key columns. */ + assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT + || eType==SQLITE_TEXT || eType==SQLITE_BLOB + ); + if( isPK ) h = HASH_APPEND(h, eType); - switch( eType ){ - case SQLITE_INTEGER: - case SQLITE_FLOAT: { - if( isPK ){ - i64 iVal = sessionGetI64(a); - h = sessionHashAppendI64(h, iVal); - } - a += 8; - break; - } - case SQLITE_TEXT: - case SQLITE_BLOB: { - int n; - a += sessionVarintGet(a, &n); - if( isPK ){ - h = sessionHashAppendBlob(h, n, a); - } - a += n; - break; - } + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + if( isPK ) h = sessionHashAppendI64(h, sessionGetI64(a)); + a += 8; + }else{ + int n; + a += sessionVarintGet(a, &n); + if( isPK ) h = sessionHashAppendBlob(h, n, a); + a += n; } } return (h % nBucket); @@ -665,6 +667,7 @@ static void sessionPreupdateOneChange( SessionChange *pChange; SessionChange *pC; int iHash; + int bNullPk = 0; int rc = SQLITE_OK; if( pSession->rc ) return; @@ -676,71 +679,73 @@ static void sessionPreupdateOneChange( if( sessionGrowHash(pSession, pTab) ) return; /* Search the hash table for an existing entry for rowid=iKey2. If - ** one is found, store a pointer to it in pChange and unlink it from - ** the hash table. Otherwise, set pChange to NULL. - */ - rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash); - for(pC=pTab->apChange[iHash]; rc==SQLITE_OK && pC; pC=pC->pNext){ - int bEqual; - rc = sessionPreupdateEqual(db, pTab, pC, op==SQLITE_INSERT, &bEqual); - if( bEqual ) break; - } - if( pC==0 ){ - /* Create a new change object containing all the old values (if - ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK - ** values (if this is an INSERT). */ - int nByte; /* Number of bytes to allocate */ - int i; /* Used to iterate through columns */ - - pTab->nEntry++; - - /* Figure out how large an allocation is required */ - nByte = sizeof(SessionChange); - for(i=0; inCol && rc==SQLITE_OK; i++){ - sqlite3_value *p = 0; - if( op!=SQLITE_INSERT ){ - rc = sqlite3_preupdate_old(pSession->db, i, &p); - }else if( 1 || pTab->abPK[i] ){ - rc = sqlite3_preupdate_new(pSession->db, i, &p); - } - if( p && rc==SQLITE_OK ){ - rc = sessionSerializeValue(0, p, &nByte); - } + ** one is found, store a pointer to it in pChange and unlink it from + ** the hash table. Otherwise, set pChange to NULL. + */ + rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk); + if( bNullPk==0 ){ + for(pC=pTab->apChange[iHash]; rc==SQLITE_OK && pC; pC=pC->pNext){ + int bEqual; + rc = sessionPreupdateEqual(db, pTab, pC, op==SQLITE_INSERT, &bEqual); + if( bEqual ) break; } - - /* Allocate the change object */ - pChange = (SessionChange *)sqlite3_malloc(nByte); - if( !pChange ){ - rc = SQLITE_NOMEM; - }else{ - memset(pChange, 0, sizeof(SessionChange)); - pChange->aRecord = (u8 *)&pChange[1]; - } - - /* Populate the change object */ - nByte = 0; - for(i=0; inCol && rc==SQLITE_OK; i++){ - sqlite3_value *p = 0; - if( op!=SQLITE_INSERT ){ - rc = sqlite3_preupdate_old(pSession->db, i, &p); - }else if( 1 || pTab->abPK[i] ){ - rc = sqlite3_preupdate_new(pSession->db, i, &p); + if( pC==0 ){ + /* Create a new change object containing all the old values (if + ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK + ** values (if this is an INSERT). */ + int nByte; /* Number of bytes to allocate */ + int i; /* Used to iterate through columns */ + + pTab->nEntry++; + + /* Figure out how large an allocation is required */ + nByte = sizeof(SessionChange); + for(i=0; inCol && rc==SQLITE_OK; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + rc = sqlite3_preupdate_old(pSession->db, i, &p); + }else if( 1 || pTab->abPK[i] ){ + rc = sqlite3_preupdate_new(pSession->db, i, &p); + } + if( p && rc==SQLITE_OK ){ + rc = sessionSerializeValue(0, p, &nByte); + } } - if( p && rc==SQLITE_OK ){ - rc = sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte); + + /* Allocate the change object */ + pChange = (SessionChange *)sqlite3_malloc(nByte); + if( !pChange ){ + rc = SQLITE_NOMEM; + }else{ + memset(pChange, 0, sizeof(SessionChange)); + pChange->aRecord = (u8 *)&pChange[1]; + } + + /* Populate the change object */ + nByte = 0; + for(i=0; inCol && rc==SQLITE_OK; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + rc = sqlite3_preupdate_old(pSession->db, i, &p); + }else if( 1 || pTab->abPK[i] ){ + rc = sqlite3_preupdate_new(pSession->db, i, &p); + } + if( p && rc==SQLITE_OK ){ + rc = sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte); + } + } + pChange->nRecord = nByte; + + /* If an error has occurred, mark the session object as failed. */ + if( rc!=SQLITE_OK ){ + sqlite3_free(pChange); + pSession->rc = rc; + }else{ + /* Add the change back to the hash-table */ + pChange->bInsert = (op==SQLITE_INSERT); + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; } - } - pChange->nRecord = nByte; - - /* If an error has occurred, mark the session object as failed. */ - if( rc!=SQLITE_OK ){ - sqlite3_free(pChange); - pSession->rc = rc; - }else{ - /* Add the change back to the hash-table */ - pChange->bInsert = (op==SQLITE_INSERT); - pChange->pNext = pTab->apChange[iHash]; - pTab->apChange[iHash] = pChange; } } } diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index 2819bfece6..44b58eada8 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -106,6 +106,9 @@ int sqlite3session_enable(sqlite3_session *pSession, int bEnable); ** is it an error if the named table does not have a PRIMARY KEY. However, ** no changes will be recorded in either of these scenarios. ** +** Changes are not recorded for individual rows that have NULL values stored +** in one or more of their PRIMARY KEY columns. +** ** SQLITE_OK is returned if the table is successfully attached to the session ** object. Or, if an error occurs, an SQLite error code (e.g. SQLITE_NOMEM) ** is returned. @@ -135,6 +138,16 @@ int sqlite3session_attach( ** modifies the values of primary key columns. If such a change is made, it ** is represented in a changeset as a DELETE followed by an INSERT. ** +** Changes are not recorded for rows that have NULL values stored in one or +** more of their PRIMARY KEY columns. If such a row is inserted or deleted, +** no corresponding change is present in the changesets returned by this +** function. If an existing row with one or more NULL values stored in +** PRIMARY KEY columns is updated so that all PRIMARY KEY columns are non-NULL, +** only an INSERT is appears in the changeset. Similarly, if an existing row +** with non-NULL PRIMARY KEY values is updated so that one or more of its +** PRIMARY KEY columns are set to NULL, the resulting changeset contains a +** DELETE change only. +** ** The contents of a changeset may be traversed using an iterator created ** using the [sqlite3changeset_start()] API. A changeset may be applied to ** a database with a compatible schema using the [sqlite3changeset_apply()] @@ -153,6 +166,10 @@ int sqlite3session_attach( ** recorded once - the first time a row with said primary key is inserted, ** updated or deleted in the lifetime of the session. ** +** There is one exception to the previous paragraph: when a row is inserted, +** updated or deleted, if one or more of its primary key columns contains a +** NULL value, no record of the change is made. +** ** The session object therefore accumulates two types of records - those ** that consist of primary key values only (created when the user inserts ** a new record) and those that consist of the primary key values and the diff --git a/manifest b/manifest index 46016a2034..63945fc241 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\ssession\smodule\sproblems\swith\sreal\s(floating\spoint)\svalues. -D 2011-03-21T11:03:25 +C Clarify\shandling\sof\sNULL\svalues\sin\sPK\scolumns\sin\ssqlite3session.h.\sAdd\stests\sand\sfixes\sfor\sthe\ssame. +D 2011-03-21T11:55:07 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f F Makefile.in 27701a1653595a1f2187dc61c8117e00a6c1d50f F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 @@ -100,9 +100,9 @@ F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0 F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024 F ext/session/session1.test 3f982c74ee4ba97069917cc35aae25b4ed858e6a -F ext/session/session2.test 45c9ff2052bf132d25d272b1d4b53f95c1c31463 -F ext/session/sqlite3session.c 3ed836ee8c6faff866bc59da800b6f20b0285071 -F ext/session/sqlite3session.h b77b014793162a77ac16507d720fe085cc15d06c +F ext/session/session2.test 3ef304f660b2a929e6bfec2df125c1809f5501ff +F ext/session/sqlite3session.c 70b19f80eadf7060836eaa90928f08a58aa3b35f +F ext/session/sqlite3session.h 2c071ee5925e82c21c7c9c296a0422c039607106 F ext/session/test_session.c 2559ef68e421c7fb83e2c19ef08a17343b70d535 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 @@ -921,7 +921,7 @@ F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f -P 0853e530cc8d96f025f5160540e8ab3243dea11b -R 482fc377ccc3f262af8beae8e4f384bf +P a192d04f4e3a9e4960a4d96d1d3ee8635bc1034d +R 0282a54a03134becd2cdef1929ff705c U dan -Z 767fabeb255e365fd4d794b5ace5ff7b +Z 9cc92283568709d19359195efd9bde16 diff --git a/manifest.uuid b/manifest.uuid index e7426fdf05..754c4bfa3c 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -a192d04f4e3a9e4960a4d96d1d3ee8635bc1034d \ No newline at end of file +aed4273054cbd150c86b36ea951d17c981633ba0 \ No newline at end of file