Clarify handling of NULL values in PK columns in sqlite3session.h. Add tests and fixes for the same.

FossilOrigin-Name: aed4273054cbd150c86b36ea951d17c981633ba0
This commit is contained in:
dan 2011-03-21 11:55:06 +00:00
parent 4e895da185
commit 27453faef8
5 changed files with 235 additions and 214 deletions

View File

@ -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

View File

@ -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; i<pTab->nCol; 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; i<pTab->nCol && 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; i<pTab->nCol && 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; i<pTab->nCol && 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; i<pTab->nCol && 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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
a192d04f4e3a9e4960a4d96d1d3ee8635bc1034d
aed4273054cbd150c86b36ea951d17c981633ba0