1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-29 08:01:23 +03:00

Add the sqlite3changeset_apply() function. Does not yet handle all cases.

FossilOrigin-Name: 2b19be7bf753c7dd12e1c3b384981a3ea1bc8145
This commit is contained in:
dan
2011-03-11 19:05:52 +00:00
parent 91ddd5595b
commit d5f0767c9c
6 changed files with 924 additions and 16 deletions

View File

@ -573,6 +573,47 @@ static void sessionAppendBlob(
} }
} }
static void sessionAppendStr(
SessionBuffer *p,
const char *zStr,
int *pRc
){
int nStr = strlen(zStr);
if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, nStr, pRc) ){
memcpy(&p->aBuf[p->nBuf], zStr, nStr);
p->nBuf += nStr;
}
}
static void sessionAppendInteger(
SessionBuffer *p,
int iVal,
int *pRc
){
char aBuf[24];
sqlite3_snprintf(sizeof(aBuf)-1, aBuf, "%d", iVal);
sessionAppendStr(p, aBuf, pRc);
}
static void sessionAppendIdent(
SessionBuffer *p,
const char *zStr,
int *pRc
){
int nStr = strlen(zStr)*2 + 2 + 1;
if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, nStr, pRc) ){
char *zOut = (char *)&p->aBuf[p->nBuf];
const char *zIn = zStr;
*zOut++ = '"';
while( *zIn ){
if( *zIn=='"' ) *zOut++ = '"';
*zOut++ = *(zIn++);
}
*zOut++ = '"';
p->nBuf = ((u8 *)zOut - p->aBuf);
}
}
static void sessionAppendCol( static void sessionAppendCol(
SessionBuffer *p, SessionBuffer *p,
sqlite3_stmt *pStmt, sqlite3_stmt *pStmt,
@ -616,6 +657,7 @@ static void sessionAppendUpdate(
sqlite3_stmt *pStmt, sqlite3_stmt *pStmt,
SessionBuffer *pBuf, SessionBuffer *pBuf,
SessionChange *p, SessionChange *p,
u8 *abPK,
int *pRc int *pRc
){ ){
if( *pRc==SQLITE_OK ){ if( *pRc==SQLITE_OK ){
@ -667,6 +709,9 @@ static void sessionAppendUpdate(
nCopy = nAdvance; nCopy = nAdvance;
} }
} }
if( abPK[i] ){
nCopy = nAdvance;
}
if( nCopy==0 ){ if( nCopy==0 ){
sessionAppendByte(pBuf, 0, pRc); sessionAppendByte(pBuf, 0, pRc);
@ -686,8 +731,98 @@ static void sessionAppendUpdate(
sqlite3_free(buf2.aBuf); sqlite3_free(buf2.aBuf);
} }
} }
}
static int sessionTableInfo(
sqlite3 *db, /* Database connection */
const char *zThis, /* Table name */
int nCol, /* Expected number of columns */
const char **pzTab, /* OUT: Copy of zThis */
const char ***pazCol, /* OUT: Array of column names for table */
u8 **pabPK /* OUT: Array of booleans - true for PK col */
){
char *zPragma;
sqlite3_stmt *pStmt;
int rc;
int nByte;
int nDbCol = 0;
int nThis;
int i;
u8 *pAlloc;
u8 *pFree = 0;
char **azCol;
u8 *abPK;
nThis = strlen(zThis);
zPragma = sqlite3_mprintf("PRAGMA main.table_info('%q')", zThis);
if( !zPragma ) return SQLITE_NOMEM;
rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
sqlite3_free(zPragma);
if( rc!=SQLITE_OK ) return rc;
nByte = nThis + 1;
while( SQLITE_ROW==sqlite3_step(pStmt) ){
nByte += sqlite3_column_bytes(pStmt, 1);
nDbCol++;
}
rc = sqlite3_reset(pStmt);
if( nDbCol!=nCol ){
rc = SQLITE_SCHEMA;
}
if( rc==SQLITE_OK ){
nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1);
pAlloc = sqlite3_malloc(nByte);
if( pAlloc==0 ){
rc = SQLITE_NOMEM;
}
}
if( rc==SQLITE_OK ){
pFree = pAlloc;
if( pazCol ){
azCol = (char **)pAlloc;
pAlloc = (u8 *)&azCol[nCol];
}
if( pabPK ){
abPK = (u8 *)pAlloc;
pAlloc = &abPK[nCol];
}
if( pzTab ){
memcpy(pAlloc, zThis, nThis+1);
*pzTab = (char *)pAlloc;
pAlloc += nThis+1;
}
i = 0;
while( SQLITE_ROW==sqlite3_step(pStmt) ){
int nName = sqlite3_column_bytes(pStmt, 1);
const unsigned char *zName = sqlite3_column_text(pStmt, 1);
if( zName==0 ) break;
if( pazCol ){
memcpy(pAlloc, zName, nName+1);
azCol[i] = (char *)pAlloc;
pAlloc += nName+1;
}
if( pabPK ) abPK[i] = sqlite3_column_int(pStmt, 5);
i++;
}
rc = sqlite3_reset(pStmt);
}
if( rc==SQLITE_OK ){
if( pazCol ) *pazCol = (const char **)azCol;
if( pabPK ) *pabPK = abPK;
}else{
if( pazCol ) *pazCol = 0;
if( pabPK ) *pabPK = 0;
if( pzTab ) *pzTab = 0;
sqlite3_free(pFree);
}
sqlite3_finalize(pStmt);
return rc;
} }
/* /*
@ -717,6 +852,7 @@ int sqlite3session_changeset(
sqlite3_stmt *pStmt = 0; sqlite3_stmt *pStmt = 0;
int bNoop = 1; int bNoop = 1;
int nRewind = buf.nBuf; int nRewind = buf.nBuf;
u8 *abPK = 0;
/* Write a table header */ /* Write a table header */
sessionAppendByte(&buf, 'T', &rc); sessionAppendByte(&buf, 'T', &rc);
@ -740,6 +876,10 @@ int sqlite3session_changeset(
rc = SQLITE_SCHEMA; rc = SQLITE_SCHEMA;
} }
if( rc==SQLITE_OK ){
rc = sessionTableInfo(db, pTab->zName, pTab->nCol, 0, 0, &abPK);
}
for(i=0; i<pTab->nChange; i++){ for(i=0; i<pTab->nChange; i++){
SessionChange *p; SessionChange *p;
for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){ for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){
@ -747,7 +887,7 @@ int sqlite3session_changeset(
if( sqlite3_step(pStmt)==SQLITE_ROW ){ if( sqlite3_step(pStmt)==SQLITE_ROW ){
int iCol; int iCol;
if( p->aRecord ){ if( p->aRecord ){
sessionAppendUpdate(pStmt, &buf, p, &rc); sessionAppendUpdate(pStmt, &buf, p, abPK, &rc);
}else{ }else{
sessionAppendByte(&buf, SQLITE_INSERT, &rc); sessionAppendByte(&buf, SQLITE_INSERT, &rc);
for(iCol=0; iCol<pTab->nCol; iCol++){ for(iCol=0; iCol<pTab->nCol; iCol++){
@ -766,6 +906,7 @@ int sqlite3session_changeset(
} }
sqlite3_finalize(pStmt); sqlite3_finalize(pStmt);
sqlite3_free(abPK);
if( bNoop ){ if( bNoop ){
buf.nBuf = nRewind; buf.nBuf = nRewind;
@ -797,6 +938,7 @@ struct sqlite3_changeset_iter {
u8 *pNext; /* Pointer to next change within aChangeset */ u8 *pNext; /* Pointer to next change within aChangeset */
int rc; int rc;
sqlite3_stmt *pConflict; /* Conflicting row, if any */
char *zTab; /* Current table */ char *zTab; /* Current table */
int nCol; /* Number of columns in zTab */ int nCol; /* Number of columns in zTab */
int op; /* Current operation */ int op; /* Current operation */
@ -947,9 +1089,9 @@ int sqlite3changeset_next(sqlite3_changeset_iter *p){
*/ */
int sqlite3changeset_op( int sqlite3changeset_op(
sqlite3_changeset_iter *pIter, sqlite3_changeset_iter *pIter,
const char **pzTab, /* OUT: Pointer to table name */ const char **pzTab, /* OUT: Pointer to table name */
int *pnCol, /* OUT: Number of columns in table */ int *pnCol, /* OUT: Number of columns in table */
int *pOp /* OUT: SQLITE_INSERT, DELETE or UPDATE */ int *pOp /* OUT: SQLITE_INSERT, DELETE or UPDATE */
){ ){
*pOp = pIter->op; *pOp = pIter->op;
*pnCol = pIter->nCol; *pnCol = pIter->nCol;
@ -960,8 +1102,11 @@ int sqlite3changeset_op(
int sqlite3changeset_old( int sqlite3changeset_old(
sqlite3_changeset_iter *pIter, sqlite3_changeset_iter *pIter,
int iVal, int iVal,
sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */
){ ){
if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_DELETE ){
return SQLITE_MISUSE;
}
if( iVal<0 || iVal>=pIter->nCol ){ if( iVal<0 || iVal>=pIter->nCol ){
return SQLITE_RANGE; return SQLITE_RANGE;
} }
@ -972,8 +1117,11 @@ int sqlite3changeset_old(
int sqlite3changeset_new( int sqlite3changeset_new(
sqlite3_changeset_iter *pIter, sqlite3_changeset_iter *pIter,
int iVal, int iVal,
sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */
){ ){
if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_INSERT ){
return SQLITE_MISUSE;
}
if( iVal<0 || iVal>=pIter->nCol ){ if( iVal<0 || iVal>=pIter->nCol ){
return SQLITE_RANGE; return SQLITE_RANGE;
} }
@ -981,6 +1129,21 @@ int sqlite3changeset_new(
return SQLITE_OK; return SQLITE_OK;
} }
int sqlite3changeset_conflict(
sqlite3_changeset_iter *pIter,
int iVal,
sqlite3_value **ppValue /* OUT: Value from conflicting row */
){
if( !pIter->pConflict ){
return SQLITE_MISUSE;
}
if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){
return SQLITE_RANGE;
}
*ppValue = sqlite3_column_value(pIter->pConflict, iVal);
return SQLITE_OK;
}
/* /*
** Finalize an iterator allocated with sqlite3changeset_start(). ** Finalize an iterator allocated with sqlite3changeset_start().
** **
@ -1073,5 +1236,436 @@ int sqlite3changeset_invert(
return SQLITE_OK; return SQLITE_OK;
} }
static void sessionUpdateDeleteWhere(
SessionBuffer *pBuf, /* Buffer to append to */
int nCol, /* Number of entries in azCol and abPK */
const char **azCol, /* Column names */
u8 *abPK, /* True for PK columns */
int *pRc /* IN/OUT: Error code */
){
if( *pRc==SQLITE_OK ){
int i;
const char *zSep = "";
sessionAppendStr(pBuf, " WHERE ", pRc);
for(i=0; i<nCol; i++){
if( abPK[i] ){
sessionAppendStr(pBuf, zSep, pRc);
sessionAppendIdent(pBuf, azCol[i], pRc);
sessionAppendStr(pBuf, " = ?", pRc);
sessionAppendInteger(pBuf, i+1, pRc);
zSep = "AND ";
}
}
sessionAppendStr(pBuf, " AND (?", pRc);
sessionAppendInteger(pBuf, nCol+1, pRc);
sessionAppendStr(pBuf, " OR ", pRc);
zSep = "";
for(i=0; i<nCol; i++){
if( !abPK[i] ){
sessionAppendStr(pBuf, zSep, pRc);
sessionAppendIdent(pBuf, azCol[i], pRc);
sessionAppendStr(pBuf, " IS ?", pRc);
sessionAppendInteger(pBuf, i+1, pRc);
zSep = "AND ";
}
}
sessionAppendStr(pBuf, ")", pRc);
}
}
/*
** Formulate a statement to DELETE a row from database db. Assuming a table
** structure like this:
**
** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
**
** The DELETE statement looks like this:
**
** DELETE FROM x WHERE a = :1 AND c = :3 AND :5 OR (b IS :2 AND d IS :4)
**
** Variable :5 (nCol+1) is a boolean. It should be set to 0 if we require
** matching b and d values, or 1 otherwise. The second case comes up if the
** conflict handler is invoked with NOTFOUND and returns CHANGESET_REPLACE.
*/
static int sessionDeleteRow(
sqlite3 *db, /* Database handle */
const char *zTab, /* Table name */
int nCol, /* Number of entries in azCol and abPK */
const char **azCol, /* Column names */
u8 *abPK, /* True for PK columns */
sqlite3_stmt **ppStmt /* OUT: Compiled SELECT statement. */
){
int rc = SQLITE_OK;
if( *ppStmt==0 ){
SessionBuffer buf = {0, 0, 0};
sessionAppendStr(&buf, "DELETE FROM ", &rc);
sessionAppendIdent(&buf, zTab, &rc);
sessionUpdateDeleteWhere(&buf, nCol, azCol, abPK, &rc);
if( rc==SQLITE_OK ){
rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
}
sqlite3_free(buf.aBuf);
}
return rc;
}
/*
** Formulate and prepare a statement to UPDATE a row from database db.
** Assuming a table structure like this:
**
** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
**
** The UPDATE statement looks like this:
**
** UPDATE x SET
** a = CASE WHEN ?2 THEN ?3 ELSE a END,
** b = CASE WHEN ?5 THEN ?6 ELSE a END,
** c = CASE WHEN ?8 THEN ?9 ELSE a END,
** d = CASE WHEN ?11 THEN ?12 ELSE a END
** WHERE a = ?1 AND c = ?7 AND (?13 OR
** (?5==0 OR b IS ?4) AND (?11==0 OR b IS ?10) AND
** )
**
** For each column in the table, there are three variables to bind:
**
** ?(i*3+1) The old.* value of the column, if any.
** ?(i*3+2) A boolean flag indicating that the value is being modified.
** ?(i*3+3) The new.* value of the column, if any.
**
** Also, a boolean flag that, if set to true, causes the statement to update
** a row even if the non-PK values do not match. This is required if the
** conflict-handler is invoked with CHANGESET_DATA and returns
** CHANGESET_REPLACE. This is variable "?(nCol*3+1)".
**
*/
static int sessionUpdateRow(
sqlite3 *db, /* Database handle */
const char *zTab, /* Table name */
int nCol, /* Number of entries in azCol and abPK */
const char **azCol, /* Column names */
u8 *abPK, /* True for PK columns */
sqlite3_stmt **ppStmt /* OUT: Compiled SELECT statement. */
){
int rc = SQLITE_OK;
if( *ppStmt==0 ){
int i;
const char *zSep = "";
SessionBuffer buf = {0, 0, 0};
/* Append "UPDATE tbl SET " */
sessionAppendStr(&buf, "UPDATE ", &rc);
sessionAppendIdent(&buf, zTab, &rc);
sessionAppendStr(&buf, " SET ", &rc);
/* Append the assignments */
for(i=0; i<nCol; i++){
sessionAppendStr(&buf, zSep, &rc);
sessionAppendIdent(&buf, azCol[i], &rc);
sessionAppendStr(&buf, " = CASE WHEN ?", &rc);
sessionAppendInteger(&buf, i*3+2, &rc);
sessionAppendStr(&buf, " THEN ?", &rc);
sessionAppendInteger(&buf, i*3+3, &rc);
sessionAppendStr(&buf, " ELSE ", &rc);
sessionAppendIdent(&buf, azCol[i], &rc);
sessionAppendStr(&buf, " END", &rc);
zSep = ", ";
}
/* Append the PK part of the WHERE clause */
sessionAppendStr(&buf, " WHERE ", &rc);
for(i=0; i<nCol; i++){
if( abPK[i] ){
sessionAppendIdent(&buf, azCol[i], &rc);
sessionAppendStr(&buf, " = ?", &rc);
sessionAppendInteger(&buf, i*3+1, &rc);
sessionAppendStr(&buf, " AND ", &rc);
}
}
/* Append the non-PK part of the WHERE clause */
sessionAppendStr(&buf, " (?", &rc);
sessionAppendInteger(&buf, nCol*3+1, &rc);
sessionAppendStr(&buf, " OR 1", &rc);
for(i=0; i<nCol; i++){
if( !abPK[i] ){
sessionAppendStr(&buf, " AND (?", &rc);
sessionAppendInteger(&buf, i*3+2, &rc);
sessionAppendStr(&buf, "=0 OR ", &rc);
sessionAppendIdent(&buf, azCol[i], &rc);
sessionAppendStr(&buf, " IS ?", &rc);
sessionAppendInteger(&buf, i*3+1, &rc);
sessionAppendStr(&buf, ")", &rc);
}
}
sessionAppendStr(&buf, ")", &rc);
if( rc==SQLITE_OK ){
rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
}
sqlite3_free(buf.aBuf);
}
return rc;
}
static int sessionSelectRow(
sqlite3 *db, /* Database handle */
const char *zTab, /* Table name */
int nCol, /* Number of entries in azCol and abPK */
const char **azCol, /* Column names */
u8 *abPK, /* True for PK columns */
sqlite3_stmt **ppStmt /* OUT: Compiled SELECT statement. */
){
int rc = SQLITE_OK;
if( *ppStmt==0 ){
int i;
const char *zSep = "";
SessionBuffer buf = {0, 0, 0};
sessionAppendStr(&buf, "SELECT * FROM ", &rc);
sessionAppendIdent(&buf, zTab, &rc);
sessionAppendStr(&buf, " WHERE ", &rc);
for(i=0; i<nCol; i++){
if( abPK[i] ){
sessionAppendStr(&buf, zSep, &rc);
sessionAppendIdent(&buf, azCol[i], &rc);
sessionAppendStr(&buf, " = ?", &rc);
sessionAppendInteger(&buf, i+1, &rc);
zSep = " AND ";
}
}
if( rc==SQLITE_OK ){
rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
}
sqlite3_free(buf.aBuf);
}
return rc;
}
int sqlite3changeset_apply(
sqlite3 *db,
int nChangeset,
void *pChangeset,
int(*xConflict)(
void *pCtx, /* Copy of fifth arg to _apply() */
int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
sqlite3_changeset_iter *p /* Handle describing change and conflict */
),
void *pCtx
){
sqlite3_changeset_iter *pIter;
int rc;
int rc2;
const char *zTab = 0; /* Name of current table */
int nTab = 0; /* Result of strlen(zTab) */
int nCol = 0; /* Number of columns in table zTab */
const char **azCol = 0; /* Array of column names */
u8 *abPK = 0; /* Boolean array - true if column is in PK */
sqlite3_stmt *pDelete = 0; /* DELETE statement */
sqlite3_stmt *pUpdate = 0; /* DELETE statement */
sqlite3_stmt *pInsert = 0; /* INSERT statement */
sqlite3_stmt *pSelect = 0; /* SELECT statement */
rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0);
sqlite3changeset_start(&pIter, nChangeset, pChangeset);
while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
int op;
const char *zThis;
sqlite3changeset_op(pIter, &zThis, &nCol, &op);
if( zTab==0 || sqlite3_strnicmp(zThis, zTab, nTab+1) ){
sqlite3_free(azCol);
rc = sessionTableInfo(db, zThis, nCol, &zTab, &azCol, &abPK);
nTab = strlen(zTab);
sqlite3_finalize(pDelete);
sqlite3_finalize(pUpdate);
sqlite3_finalize(pInsert);
sqlite3_finalize(pSelect);
pSelect = pUpdate = pInsert = pDelete = 0;
}
if( op==SQLITE_DELETE ){
int res;
int i;
rc = sessionDeleteRow(db, zTab, nCol, azCol, abPK, &pDelete);
for(i=0; rc==SQLITE_OK && i<nCol; i++){
sqlite3_value *pVal;
rc = sqlite3changeset_old(pIter, i, &pVal);
if( rc==SQLITE_OK ){
rc = sqlite3_bind_value(pDelete, i+1, pVal);
}
}
if( rc==SQLITE_OK ) rc = sqlite3_bind_int(pDelete, nCol+1, 0);
if( rc!=SQLITE_OK ) break;
sqlite3_step(pDelete);
rc = sqlite3_reset(pDelete);
if( rc==SQLITE_OK && sqlite3_changes(db)==0 ){
/* A NOTFOUND or DATA error. Search the table to see if it contains
** a row with a matching primary key. If so, this is a DATA conflict.
** Otherwise, if there is no primary key match, it is a NOTFOUND. */
rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
for(i=0; rc==SQLITE_OK && i<nCol; i++){
if( abPK[i] ){
sqlite3_value *pVal;
rc = sqlite3changeset_old(pIter, i, &pVal);
if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
}
}
if( rc!=SQLITE_OK ) break;
if( SQLITE_ROW==sqlite3_step(pSelect) ){
pIter->pConflict = pSelect;
res = xConflict(pCtx, SQLITE_CHANGESET_DATA, pIter);
pIter->pConflict = 0;
sqlite3_reset(pSelect);
}else{
rc = sqlite3_reset(pSelect);
if( rc==SQLITE_OK ){
res = xConflict(pCtx, SQLITE_CHANGESET_NOTFOUND, pIter);
}
}
}else if( rc==SQLITE_CONSTRAINT ){
res = xConflict(pCtx, SQLITE_CHANGESET_CONSTRAINT, pIter);
rc = SQLITE_OK;
}
if( rc!=SQLITE_OK ) break;
}else if( op==SQLITE_UPDATE ){
int i;
int res;
rc = sessionUpdateRow(db, zTab, nCol, azCol, abPK, &pUpdate);
for(i=0; rc==SQLITE_OK && i<nCol; i++){
sqlite3_value *pOld = 0;
sqlite3_value *pNew = 0;
rc = sqlite3changeset_old(pIter, i, &pOld);
if( rc==SQLITE_OK ){
rc = sqlite3changeset_new(pIter, i, &pNew);
}
if( rc==SQLITE_OK ){
if( pOld ) sqlite3_bind_value(pUpdate, i*3+1, pOld);
sqlite3_bind_int(pUpdate, i*3+2, !!pNew);
if( pNew ) sqlite3_bind_value(pUpdate, i*3+3, pOld);
}
}
if( rc==SQLITE_OK ) rc = sqlite3_bind_int(pUpdate, nCol*3+1, 0);
if( rc!=SQLITE_OK ) break;
sqlite3_step(pUpdate);
rc = sqlite3_reset(pUpdate);
if( rc==SQLITE_OK && sqlite3_changes(db)==0 ){
/* A NOTFOUND or DATA error. Search the table to see if it contains
** a row with a matching primary key. If so, this is a DATA conflict.
** Otherwise, if there is no primary key match, it is a NOTFOUND. */
rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
for(i=0; rc==SQLITE_OK && i<nCol; i++){
if( abPK[i] ){
sqlite3_value *pVal;
rc = sqlite3changeset_old(pIter, i, &pVal);
if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
}
}
if( rc!=SQLITE_OK ) break;
if( SQLITE_ROW==sqlite3_step(pSelect) ){
pIter->pConflict = pSelect;
res = xConflict(pCtx, SQLITE_CHANGESET_DATA, pIter);
pIter->pConflict = 0;
sqlite3_reset(pSelect);
}else{
rc = sqlite3_reset(pSelect);
if( rc==SQLITE_OK ){
res = xConflict(pCtx, SQLITE_CHANGESET_NOTFOUND, pIter);
}
}
}else if( rc==SQLITE_CONSTRAINT ){
assert(0);
}
}else{
int i;
assert( op==SQLITE_INSERT );
if( pInsert==0 ){
SessionBuffer buf = {0, 0, 0};
sessionAppendStr(&buf, "INSERT INTO main.", &rc);
sessionAppendIdent(&buf, zTab, &rc);
sessionAppendStr(&buf, " VALUES(?", &rc);
for(i=1; i<nCol; i++) sessionAppendStr(&buf, ", ?", &rc);
sessionAppendStr(&buf, ")", &rc);
if( rc==SQLITE_OK ){
rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &pInsert, 0);
}
sqlite3_free(buf.aBuf);
}
for(i=0; rc==SQLITE_OK && i<nCol; i++){
sqlite3_value *pVal;
rc = sqlite3changeset_new(pIter, i, &pVal);
if( rc==SQLITE_OK ){
rc = sqlite3_bind_value(pInsert, i+1, pVal);
}
}
if( rc!=SQLITE_OK ) break;
sqlite3_step(pInsert);
rc = sqlite3_reset(pInsert);
if( rc==SQLITE_CONSTRAINT && xConflict ){
int res;
/* Figure out if this is a primary key or other constraint. */
rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
for(i=0; rc==SQLITE_OK && i<nCol; i++){
if( abPK[i] ){
sqlite3_value *pVal;
rc = sqlite3changeset_new(pIter, i, &pVal);
if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
}
}
if( rc!=SQLITE_OK ) break;
if( SQLITE_ROW==sqlite3_step(pSelect) ){
pIter->pConflict = pSelect;
res = xConflict(pCtx, SQLITE_CHANGESET_CONFLICT, pIter);
pIter->pConflict = 0;
sqlite3_reset(pSelect);
}else{
rc = sqlite3_reset(pSelect);
if( rc==SQLITE_OK ){
res = xConflict(pCtx, SQLITE_CHANGESET_CONSTRAINT, pIter);
}
}
}
}
}
rc2 = sqlite3changeset_finalize(pIter);
if( rc==SQLITE_DONE ) rc = rc2;
if( rc==SQLITE_OK ){
rc = sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
}else{
sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0);
sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
}
sqlite3_finalize(pInsert);
sqlite3_finalize(pDelete);
sqlite3_finalize(pUpdate);
sqlite3_finalize(pSelect);
sqlite3_free(azCol);
return rc;
}
#endif /* #ifdef SQLITE_ENABLE_SESSION */ #endif /* #ifdef SQLITE_ENABLE_SESSION */

View File

@ -112,6 +112,21 @@ int sqlite3changeset_new(
int iVal, int iVal,
sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */
); );
/*
** This function is only usable with sqlite3_changeset_iter objects passed
** to the xConflict callback by sqlite3changeset_apply(). It cannot be used
** with iterators created using sqlite3changeset_start().
**
** It is used to access the "conflicting row" information available to the
** conflict handler if the second argument is either SQLITE_CHANGESET_DATA
** or SQLITE_CHANGESET_CONFLICT.
*/
int sqlite3changeset_conflict(
sqlite3_changeset_iter *pIter,
int iVal,
sqlite3_value **ppValue /* OUT: Value from conflicting row */
);
/* /*
** Finalize an iterator allocated with sqlite3changeset_start(). ** Finalize an iterator allocated with sqlite3changeset_start().
@ -129,6 +144,36 @@ int sqlite3changeset_invert(
int *pnOut, void **ppOut /* OUT: Inverse of input */ int *pnOut, void **ppOut /* OUT: Inverse of input */
); );
/*
** Apply a changeset to a database.
**
** It is safe to execute SQL statements, including those that write to the
** table that the callback related to, from within the xConflict callback.
** This can be used to further customize the applications conflict
** resolution strategy.
*/
int sqlite3changeset_apply(
sqlite3 *db,
int nChangeset,
void *pChangeset,
int(*xConflict)(
void *pCtx, /* Copy of fifth arg to _apply() */
int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
sqlite3_changeset_iter *p /* Handle describing change and conflict */
),
void *pCtx
);
/* Values passed as the second argument to a conflict-handler */
#define SQLITE_CHANGESET_DATA 1
#define SQLITE_CHANGESET_NOTFOUND 2
#define SQLITE_CHANGESET_CONFLICT 3
#define SQLITE_CHANGESET_CONSTRAINT 4
/* Valid return values from a conflict-handler */
#define SQLITE_CHANGESET_OMIT 0
#define SQLITE_CHANGESET_REPLACE 1
#define SQLITE_CHANGESET_ABORT 2
#endif #endif

View File

@ -168,6 +168,139 @@ static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){
} }
} }
typedef struct TestConflictHandler TestConflictHandler;
struct TestConflictHandler {
Tcl_Interp *interp;
Tcl_Obj *pScript;
};
static int test_conflict_handler(
void *pCtx, /* Pointer to TestConflictHandler structure */
int eConf, /* DATA, MISSING, CONFLICT, CONSTRAINT */
sqlite3_changeset_iter *pIter /* Handle describing change and conflict */
){
TestConflictHandler *p = (TestConflictHandler *)pCtx;
Tcl_Obj *pEval;
Tcl_Interp *interp = p->interp;
int op; /* SQLITE_UPDATE, DELETE or INSERT */
const char *zTab; /* Name of table conflict is on */
int nCol; /* Number of columns in table zTab */
pEval = Tcl_DuplicateObj(p->pScript);
Tcl_IncrRefCount(pEval);
sqlite3changeset_op(pIter, &zTab, &nCol, &op);
/* Append the operation type. */
Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(
op==SQLITE_INSERT ? "INSERT" :
op==SQLITE_UPDATE ? "UPDATE" :
"DELETE", -1
));
/* Append the table name. */
Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1));
/* Append the conflict type. */
switch( eConf ){
case SQLITE_CHANGESET_DATA:
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1));
break;
case SQLITE_CHANGESET_NOTFOUND:
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1));
break;
case SQLITE_CHANGESET_CONFLICT:
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1));
break;
case SQLITE_CHANGESET_CONSTRAINT:
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1));
break;
}
/* If this is not an INSERT, append the old row */
if( op!=SQLITE_INSERT ){
int i;
Tcl_Obj *pOld = Tcl_NewObj();
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
sqlite3changeset_old(pIter, i, &pVal);
test_append_value(pOld, pVal);
}
Tcl_ListObjAppendElement(0, pEval, pOld);
}
/* If this is not a DELETE, append the new row */
if( op!=SQLITE_DELETE ){
int i;
Tcl_Obj *pNew = Tcl_NewObj();
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
sqlite3changeset_new(pIter, i, &pVal);
test_append_value(pNew, pVal);
}
Tcl_ListObjAppendElement(0, pEval, pNew);
}
/* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append
** the conflicting row. */
if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){
int i;
Tcl_Obj *pConflict = Tcl_NewObj();
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
sqlite3changeset_conflict(pIter, i, &pVal);
test_append_value(pConflict, pVal);
}
Tcl_ListObjAppendElement(0, pEval, pConflict);
}
if( TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) ){
Tcl_BackgroundError(interp);
}
Tcl_DecrRefCount(pEval);
return SQLITE_CHANGESET_OMIT;
}
/*
** sqlite3changeset_apply DB CHANGESET SCRIPT
*/
static int test_sqlite3changeset_apply(
void * clientData,
Tcl_Interp *interp,
int objc,
Tcl_Obj *CONST objv[]
){
sqlite3 *db; /* Database handle */
Tcl_CmdInfo info; /* Database Tcl command (objv[1]) info */
int rc; /* Return code from changeset_invert() */
void *pChangeset; /* Buffer containing changeset */
int nChangeset; /* Size of buffer aChangeset in bytes */
TestConflictHandler ctx;
if( objc!=4 ){
Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET SCRIPT");
return TCL_ERROR;
}
if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
return TCL_ERROR;
}
db = *(sqlite3 **)info.objClientData;
pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
ctx.pScript = objv[3];
ctx.interp = interp;
rc = sqlite3changeset_apply(
db, nChangeset, pChangeset, test_conflict_handler, (void *)&ctx
);
if( rc!=SQLITE_OK ){
return test_session_error(interp, rc);
}
Tcl_ResetResult(interp);
return TCL_OK;
}
/* /*
** sqlite3changeset_invert CHANGESET ** sqlite3changeset_invert CHANGESET
*/ */
@ -185,6 +318,7 @@ static int test_sqlite3changeset_invert(
if( objc!=2 ){ if( objc!=2 ){
Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET"); Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
return TCL_ERROR;
} }
aChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeSet); aChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeSet);
@ -283,6 +417,9 @@ int TestSession_Init(Tcl_Interp *interp){
Tcl_CreateObjCommand( Tcl_CreateObjCommand(
interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0 interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
); );
Tcl_CreateObjCommand(
interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
);
return TCL_OK; return TCL_OK;
} }

View File

@ -1,5 +1,5 @@
C Add\sa\sfunction\sto\sthe\ssession\sextension\sinvert\sa\schangeset. C Add\sthe\ssqlite3changeset_apply()\sfunction.\sDoes\snot\syet\shandle\sall\scases.
D 2011-03-09T11:17:05 D 2011-03-11T19:05:52
F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
F Makefile.in 27701a1653595a1f2187dc61c8117e00a6c1d50f F Makefile.in 27701a1653595a1f2187dc61c8117e00a6c1d50f
F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@ -98,9 +98,9 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0 F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0
F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024 F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
F ext/session/sqlite3session.c 82d86bbdfb0e0b731c4e7f9ab9a418340205abe1 F ext/session/sqlite3session.c 724a064f0d9a909c12dfb668b42ce007378f352e
F ext/session/sqlite3session.h d7fe09ca2e12fa6007f7df8344249111c9c78e5f F ext/session/sqlite3session.h 3246613b20857e58f7419e4e26dbe9161677aff0
F ext/session/test_session.c 7bc88e75a78248dbde3a666f7c25cb228542a60c F ext/session/test_session.c 1b4f278d0ae164e2d02c11f5e1f2df3a2567ba41
F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895
F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
F main.mk ae0868e05c76eaa8a0ae3d6927a949b1c8e810d7 F main.mk ae0868e05c76eaa8a0ae3d6927a949b1c8e810d7
@ -639,7 +639,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532
F test/selectB.test f305cc6660804cb239aab4e2f26b0e288b59958b F test/selectB.test f305cc6660804cb239aab4e2f26b0e288b59958b
F test/selectC.test f9bf1bc4581b5b8158caa6e4e4f682acb379fb25 F test/selectC.test f9bf1bc4581b5b8158caa6e4e4f682acb379fb25
F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c
F test/session1.test f0b04d2840b6a2fc9893096a1e1dadcd4f241993 F test/session1.test 6a2e0db809e44879cec5314a6fd238ad768d9723
F test/shared.test b9114eaea7e748a3a4c8ff7b9ca806c8f95cef3e F test/shared.test b9114eaea7e748a3a4c8ff7b9ca806c8f95cef3e
F test/shared2.test 7f6ad2d857d0f4e5d6a0b9a897b5e56a6b6ea18c F test/shared2.test 7f6ad2d857d0f4e5d6a0b9a897b5e56a6b6ea18c
F test/shared3.test d69bdd5f156580876c5345652d21dc2092e85962 F test/shared3.test d69bdd5f156580876c5345652d21dc2092e85962
@ -913,7 +913,7 @@ F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff
F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224 F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224
F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e
F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
P 269a81a37d7dbdcdec3c2322074916af0fbac91c P 75d5dff725dbb66d67d56ad042926f1daae56dbe
R 06c861473767f41ba98f1b1628042bc2 R 7203b6a6763bad83d70ecbe2db2167c5
U dan U dan
Z 8e42230c84fa0976b233cb2ac7764b8c Z 77d8992b93ad5a2a0231fb47a635b3cb

View File

@ -1 +1 @@
75d5dff725dbb66d67d56ad042926f1daae56dbe 2b19be7bf753c7dd12e1c3b384981a3ea1bc8145

View File

@ -147,4 +147,136 @@ do_changeset_test 2.4.2 S {}
do_changeset_invert_test 2.4.3 S {} do_changeset_invert_test 2.4.3 S {}
do_test 2.4.4 { S delete } {} do_test 2.4.4 { S delete } {}
#-------------------------------------------------------------------------
# Test the application of simple changesets.
#
db close
forcedelete test.db test.db2
sqlite3 db test.db
sqlite3 db2 test.db2
proc xConflict {args} {
lappend ::xConflict $args
return ""
}
proc do_conflict_test {tn args} {
set O(-tables) [list]
set O(-sql) [list]
set O(-conflicts) [list]
array set V $args
foreach key [array names V] {
if {![info exists O($key)]} {error "no such option: $key"}
}
array set O $args
sqlite3session S db main
foreach t $O(-tables) { S attach $t }
execsql $O(-sql)
set ::xConflict [list]
sqlite3changeset_apply db2 [S changeset] xConflict
set conflicts [list]
foreach c $O(-conflicts) {
lappend conflicts $c
}
uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
S delete
}
proc do_db2_test {testname sql {result {}}} {
uplevel do_test $testname [list "execsql {$sql} db2"] [list [list {*}$result]]
}
# Test INSERT changesets.
#
do_test 3.1.0 {
execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) } db2
execsql {
CREATE TABLE t1(a PRIMARY KEY, b);
INSERT INTO t1 VALUES(1, 'one');
INSERT INTO t1 VALUES(2, 'two');
} db
} {}
do_db2_test 3.1.1 "INSERT INTO t1 VALUES(6, 'VI')"
do_conflict_test 3.2.2 -tables t1 -sql {
INSERT INTO t1 VALUES(3, 'three');
INSERT INTO t1 VALUES(4, 'four');
INSERT INTO t1 VALUES(5, 'five');
INSERT INTO t1 VALUES(6, 'six');
INSERT INTO t1 VALUES(7, 'seven');
INSERT INTO t1 VALUES(8, NULL);
} -conflicts {
{INSERT t1 CONFLICT {i 6 t six} {i 6 t VI}}
{INSERT t1 CONSTRAINT {i 8 n {}}}
}
do_db2_test 3.1.2 "SELECT * FROM t1" {
6 VI 3 three 4 four 5 five 7 seven
}
do_execsql_test 3.1.3 "SELECT * FROM t1" {
1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 {}
}
# Test DELETE changesets.
#
do_execsql_test 3.2.1 {
PRAGMA foreign_keys = on;
CREATE TABLE t2(a PRIMARY KEY, b);
CREATE TABLE t3(c, d REFERENCES t2);
INSERT INTO t2 VALUES(1, 'one');
INSERT INTO t2 VALUES(2, 'two');
INSERT INTO t2 VALUES(3, 'three');
INSERT INTO t2 VALUES(4, 'four');
}
do_db2_test 3.2.2 {
PRAGMA foreign_keys = on;
CREATE TABLE t2(a PRIMARY KEY, b);
CREATE TABLE t3(c, d REFERENCES t2);
INSERT INTO t2 VALUES(1, 'one');
INSERT INTO t2 VALUES(2, 'two');
INSERT INTO t2 VALUES(4, 'five');
INSERT INTO t3 VALUES('i', 1);
}
do_conflict_test 3.2.3 -tables t2 -sql {
DELETE FROM t2 WHERE a = 1;
DELETE FROM t2 WHERE a = 2;
DELETE FROM t2 WHERE a = 3;
DELETE FROM t2 WHERE a = 4;
} -conflicts {
{DELETE t2 CONSTRAINT {i 1 t one}}
{DELETE t2 NOTFOUND {i 3 t three}}
{DELETE t2 DATA {i 4 t four} {i 4 t five}}
}
do_execsql_test 3.2.4 "SELECT * FROM t2" {}
do_db2_test 3.2.5 "SELECT * FROM t2" {1 one 4 five}
# Test UPDATE changesets.
#
do_execsql_test 3.3.1 {
CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c));
INSERT INTO t4 VALUES(1, 2, 3);
INSERT INTO t4 VALUES(4, 5, 6);
}
do_db2_test 3.3.2 {
CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c));
INSERT INTO t4 VALUES(0, 2, 3);
INSERT INTO t4 VALUES(4, 5, 7);
}
do_conflict_test 3.2.3 -tables t4 -sql {
UPDATE t4 SET a = -1 WHERE b = 2;
} -conflicts {
{UPDATE t4 DATA {i 1 i 2 i 3} {i -1 i 2 i 3} {i 0 i 2 i 3}}
}
do_db2_test 3.3.4 {
SELECT * FROM t4
} {0 2 3 4 5 7}
catch { db2 close }
finish_test finish_test