mirror of
https://github.com/sqlite/sqlite.git
synced 2025-08-05 15:55:57 +03:00
Add an experimental module to detect conflicts between sessions changesets.
FossilOrigin-Name: 0c9fd6b723041955b5182caa430312e5124fdc83
This commit is contained in:
442
ext/session/sqlite3changebatch.c
Normal file
442
ext/session/sqlite3changebatch.c
Normal file
@@ -0,0 +1,442 @@
|
||||
|
||||
#if !defined(SQLITE_TEST) || (defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK))
|
||||
|
||||
#include "sqlite3session.h"
|
||||
#include "sqlite3changebatch.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct BatchTable BatchTable;
|
||||
typedef struct BatchIndex BatchIndex;
|
||||
typedef struct BatchIndexEntry BatchIndexEntry;
|
||||
typedef struct BatchHash BatchHash;
|
||||
|
||||
struct sqlite3_changebatch {
|
||||
sqlite3 *db; /* Database handle used to read schema */
|
||||
BatchTable *pTab; /* First in linked list of tables */
|
||||
int iChangesetId; /* Current changeset id */
|
||||
int iNextIdxId; /* Next available index id */
|
||||
int nEntry; /* Number of entries in hash table */
|
||||
int nHash; /* Number of hash buckets */
|
||||
BatchIndexEntry **apHash; /* Array of hash buckets */
|
||||
};
|
||||
|
||||
struct BatchTable {
|
||||
BatchIndex *pIdx; /* First in linked list of UNIQUE indexes */
|
||||
BatchTable *pNext; /* Next table */
|
||||
char zTab[1]; /* Table name */
|
||||
};
|
||||
|
||||
struct BatchIndex {
|
||||
BatchIndex *pNext; /* Next index on same table */
|
||||
int iId; /* Index id (assigned internally) */
|
||||
int bPk; /* True for PK index */
|
||||
int nCol; /* Size of aiCol[] array */
|
||||
int *aiCol; /* Array of columns that make up index */
|
||||
};
|
||||
|
||||
struct BatchIndexEntry {
|
||||
BatchIndexEntry *pNext; /* Next colliding hash table entry */
|
||||
int iChangesetId; /* Id of associated changeset */
|
||||
int iIdxId; /* Id of index this key is from */
|
||||
int szRecord;
|
||||
char aRecord[1];
|
||||
};
|
||||
|
||||
/*
|
||||
** Allocate and zero a block of nByte bytes. Must be freed using cbFree().
|
||||
*/
|
||||
static void *cbMalloc(int *pRc, int nByte){
|
||||
void *pRet;
|
||||
|
||||
if( *pRc ){
|
||||
pRet = 0;
|
||||
}else{
|
||||
pRet = sqlite3_malloc(nByte);
|
||||
if( pRet ){
|
||||
memset(pRet, 0, nByte);
|
||||
}else{
|
||||
*pRc = SQLITE_NOMEM;
|
||||
}
|
||||
}
|
||||
|
||||
return pRet;
|
||||
}
|
||||
|
||||
/*
|
||||
** Free an allocation made by cbMalloc().
|
||||
*/
|
||||
static void cbFree(void *p){
|
||||
sqlite3_free(p);
|
||||
}
|
||||
|
||||
/*
|
||||
** Return the hash bucket that pEntry belongs in.
|
||||
*/
|
||||
static int cbHash(sqlite3_changebatch *p, BatchIndexEntry *pEntry){
|
||||
unsigned int iHash = (unsigned int)pEntry->iIdxId;
|
||||
unsigned char *pEnd = (unsigned char*)&pEntry->aRecord[pEntry->szRecord];
|
||||
unsigned char *pIter;
|
||||
|
||||
for(pIter=pEntry->aRecord; pIter<pEnd; pIter++){
|
||||
iHash += (iHash << 7) + *pIter;
|
||||
}
|
||||
|
||||
return (int)(iHash % p->nHash);
|
||||
}
|
||||
|
||||
/*
|
||||
** Resize the hash table.
|
||||
*/
|
||||
static int cbHashResize(sqlite3_changebatch *p){
|
||||
int rc = SQLITE_OK;
|
||||
BatchIndexEntry **apNew;
|
||||
int nNew = (p->nHash ? p->nHash*2 : 512);
|
||||
int i;
|
||||
|
||||
apNew = cbMalloc(&rc, sizeof(BatchIndexEntry*) * nNew);
|
||||
if( rc==SQLITE_OK ){
|
||||
int nHash = p->nHash;
|
||||
p->nHash = nNew;
|
||||
for(i=0; i<nHash; i++){
|
||||
BatchIndexEntry *pEntry;
|
||||
while( pEntry=p->apHash[i] ){
|
||||
int iHash = cbHash(p, pEntry);
|
||||
p->apHash[i] = pEntry->pNext;
|
||||
pEntry->pNext = apNew[iHash];
|
||||
apNew[iHash] = pEntry;
|
||||
}
|
||||
}
|
||||
|
||||
cbFree(p->apHash);
|
||||
p->apHash = apNew;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** Allocate a new sqlite3_changebatch object.
|
||||
*/
|
||||
int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp){
|
||||
sqlite3_changebatch *pRet;
|
||||
int rc = SQLITE_OK;
|
||||
*pp = pRet = (sqlite3_changebatch*)cbMalloc(&rc, sizeof(sqlite3_changebatch));
|
||||
if( pRet ){
|
||||
pRet->db = db;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Add a BatchIndex entry for index zIdx to table pTab.
|
||||
*/
|
||||
static int cbAddIndex(
|
||||
sqlite3_changebatch *p,
|
||||
BatchTable *pTab,
|
||||
const char *zIdx,
|
||||
int bPk
|
||||
){
|
||||
int nCol = 0;
|
||||
sqlite3_stmt *pIndexInfo = 0;
|
||||
BatchIndex *pNew = 0;
|
||||
int rc;
|
||||
char *zIndexInfo;
|
||||
|
||||
zIndexInfo = (char*)sqlite3_mprintf("PRAGMA main.index_info = %Q", zIdx);
|
||||
if( zIndexInfo ){
|
||||
rc = sqlite3_prepare_v2(p->db, zIndexInfo, -1, &pIndexInfo, 0);
|
||||
sqlite3_free(zIndexInfo);
|
||||
}else{
|
||||
rc = SQLITE_NOMEM;
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
int rc2;
|
||||
while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){ nCol++; }
|
||||
rc2 = sqlite3_reset(pIndexInfo);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
}
|
||||
|
||||
pNew = (BatchIndex*)cbMalloc(&rc, sizeof(BatchIndex) + sizeof(int) * nCol);
|
||||
if( rc==SQLITE_OK ){
|
||||
int rc2;
|
||||
pNew->nCol = nCol;
|
||||
pNew->bPk = bPk;
|
||||
pNew->aiCol = (int*)&pNew[1];
|
||||
pNew->iId = p->iNextIdxId++;
|
||||
while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){
|
||||
int i = sqlite3_column_int(pIndexInfo, 0);
|
||||
int j = sqlite3_column_int(pIndexInfo, 1);
|
||||
pNew->aiCol[i] = j;
|
||||
}
|
||||
rc2 = sqlite3_reset(pIndexInfo);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
pNew->pNext = pTab->pIdx;
|
||||
pTab->pIdx = pNew;
|
||||
}else{
|
||||
cbFree(pNew);
|
||||
}
|
||||
sqlite3_finalize(pIndexInfo);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Find or create the BatchTable object named zTab.
|
||||
*/
|
||||
static int cbFindTable(
|
||||
sqlite3_changebatch *p,
|
||||
const char *zTab,
|
||||
BatchTable **ppTab
|
||||
){
|
||||
BatchTable *pRet = 0;
|
||||
int rc = SQLITE_OK;
|
||||
|
||||
for(pRet=p->pTab; pRet; pRet=pRet->pNext){
|
||||
if( 0==sqlite3_stricmp(zTab, pRet->zTab) ) break;
|
||||
}
|
||||
|
||||
if( pRet==0 ){
|
||||
int nTab = strlen(zTab);
|
||||
pRet = (BatchTable*)cbMalloc(&rc, nTab + sizeof(BatchTable));
|
||||
if( pRet ){
|
||||
sqlite3_stmt *pIndexList = 0;
|
||||
char *zIndexList = 0;
|
||||
int rc2;
|
||||
memcpy(pRet->zTab, zTab, nTab);
|
||||
|
||||
zIndexList = sqlite3_mprintf("PRAGMA main.index_list = %Q", zTab);
|
||||
if( zIndexList==0 ){
|
||||
rc = SQLITE_NOMEM;
|
||||
}else{
|
||||
rc = sqlite3_prepare_v2(p->db, zIndexList, -1, &pIndexList, 0);
|
||||
sqlite3_free(zIndexList);
|
||||
}
|
||||
|
||||
while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pIndexList) ){
|
||||
if( sqlite3_column_int(pIndexList, 2) ){
|
||||
const char *zIdx = (const char*)sqlite3_column_text(pIndexList, 1);
|
||||
const char *zTyp = (const char*)sqlite3_column_text(pIndexList, 3);
|
||||
rc = cbAddIndex(p, pRet, zIdx, (zTyp[0]=='p'));
|
||||
}
|
||||
}
|
||||
rc2 = sqlite3_finalize(pIndexList);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
pRet->pNext = p->pTab;
|
||||
p->pTab = pRet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*ppTab = pRet;
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int cbAddToHash(
|
||||
sqlite3_changebatch *p,
|
||||
sqlite3_changeset_iter *pIter,
|
||||
BatchIndex *pIdx,
|
||||
int (*xVal)(sqlite3_changeset_iter*,int,sqlite3_value**),
|
||||
int *pbConf
|
||||
){
|
||||
BatchIndexEntry *pNew;
|
||||
int sz = pIdx->nCol;
|
||||
int i;
|
||||
int iOut = 0;
|
||||
int rc = SQLITE_OK;
|
||||
|
||||
for(i=0; rc==SQLITE_OK && i<pIdx->nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
rc = xVal(pIter, pIdx->aiCol[i], &pVal);
|
||||
if( rc==SQLITE_OK ){
|
||||
int eType = 0;
|
||||
if( pVal ){
|
||||
eType = sqlite3_value_type(pVal);
|
||||
}
|
||||
switch( eType ){
|
||||
case 0:
|
||||
case SQLITE_NULL:
|
||||
return SQLITE_OK;
|
||||
|
||||
case SQLITE_INTEGER:
|
||||
sz += 8;
|
||||
break;
|
||||
case SQLITE_FLOAT:
|
||||
sz += 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
|
||||
sz += sqlite3_value_bytes(pVal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pNew = cbMalloc(&rc, sizeof(BatchIndexEntry) + sz);
|
||||
if( pNew ){
|
||||
pNew->iChangesetId = p->iChangesetId;
|
||||
pNew->iIdxId = pIdx->iId;
|
||||
pNew->szRecord = sz;
|
||||
|
||||
for(i=0; rc==SQLITE_OK && i<pIdx->nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
rc = xVal(pIter, pIdx->aiCol[i], &pVal);
|
||||
if( rc==SQLITE_OK ){
|
||||
int eType = sqlite3_value_type(pVal);
|
||||
pNew->aRecord[iOut++] = eType;
|
||||
switch( eType ){
|
||||
case SQLITE_INTEGER: {
|
||||
sqlite3_int64 i64 = sqlite3_value_int64(pVal);
|
||||
memcpy(&pNew->aRecord[iOut], &i64, 8);
|
||||
iOut += 8;
|
||||
break;
|
||||
}
|
||||
case SQLITE_FLOAT: {
|
||||
double d64 = sqlite3_value_double(pVal);
|
||||
memcpy(&pNew->aRecord[iOut], &d64, sizeof(double));
|
||||
iOut += sizeof(double);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
int nByte = sqlite3_value_bytes(pVal);
|
||||
const char *z = (const char*)sqlite3_value_blob(pVal);
|
||||
memcpy(&pNew->aRecord[iOut], z, nByte);
|
||||
iOut += nByte;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && p->nEntry>=(p->nHash/2) ){
|
||||
rc = cbHashResize(p);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
BatchIndexEntry *pIter;
|
||||
int iHash = cbHash(p, pNew);
|
||||
|
||||
assert( iHash>=0 && iHash<p->nHash );
|
||||
for(pIter=p->apHash[iHash]; pIter; pIter=pIter->pNext){
|
||||
if( pNew->szRecord==pIter->szRecord
|
||||
&& 0==memcmp(pNew->aRecord, pIter->aRecord, pNew->szRecord)
|
||||
){
|
||||
if( pNew->iChangesetId!=pIter->iChangesetId ){
|
||||
*pbConf = 1;
|
||||
}
|
||||
cbFree(pNew);
|
||||
pNew = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( pNew ){
|
||||
pNew->pNext = p->apHash[iHash];
|
||||
p->apHash[iHash] = pNew;
|
||||
p->nEntry++;
|
||||
}
|
||||
}
|
||||
|
||||
p->iChangesetId++;
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** Add a changeset to the current batch.
|
||||
*/
|
||||
int sqlite3changebatch_add(sqlite3_changebatch *p, void *pBuf, int nBuf){
|
||||
sqlite3_changeset_iter *pIter; /* Iterator opened on pBuf/nBuf */
|
||||
int rc; /* Return code */
|
||||
int bConf = 0; /* Conflict was detected */
|
||||
|
||||
rc = sqlite3changeset_start(&pIter, nBuf, pBuf);
|
||||
if( rc==SQLITE_OK ){
|
||||
int rc2;
|
||||
for(rc2 = sqlite3changeset_next(pIter);
|
||||
rc2==SQLITE_ROW;
|
||||
rc2 = sqlite3changeset_next(pIter)
|
||||
){
|
||||
BatchTable *pTab;
|
||||
BatchIndex *pIdx;
|
||||
const char *zTab; /* Table this change applies to */
|
||||
int nCol; /* Number of columns in table */
|
||||
int op; /* UPDATE, INSERT or DELETE */
|
||||
|
||||
sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
|
||||
assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE );
|
||||
|
||||
rc = cbFindTable(p, zTab, &pTab);
|
||||
for(pIdx=pTab->pIdx; pIdx && rc==SQLITE_OK; pIdx=pIdx->pNext){
|
||||
if( op==SQLITE_UPDATE && pIdx->bPk ) continue;
|
||||
if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){
|
||||
rc = cbAddToHash(p, pIter, pIdx, sqlite3changeset_old, &bConf);
|
||||
}
|
||||
if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){
|
||||
rc = cbAddToHash(p, pIter, pIdx, sqlite3changeset_new, &bConf);
|
||||
}
|
||||
}
|
||||
if( rc!=SQLITE_OK ) break;
|
||||
}
|
||||
|
||||
rc2 = sqlite3changeset_finalize(pIter);
|
||||
if( rc==SQLITE_OK ) rc = rc2;
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && bConf ){
|
||||
rc = SQLITE_CONSTRAINT;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Zero an existing changebatch object.
|
||||
*/
|
||||
void sqlite3changebatch_zero(sqlite3_changebatch *p){
|
||||
int i;
|
||||
for(i=0; i<p->nHash; i++){
|
||||
BatchIndexEntry *pEntry;
|
||||
BatchIndexEntry *pNext;
|
||||
for(pEntry=p->apHash[i]; pEntry; pEntry=pNext){
|
||||
pNext = pEntry->pNext;
|
||||
cbFree(pEntry);
|
||||
}
|
||||
}
|
||||
cbFree(p->apHash);
|
||||
p->nHash = 0;
|
||||
p->apHash = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
** Delete a changebatch object.
|
||||
*/
|
||||
void sqlite3changebatch_delete(sqlite3_changebatch *p){
|
||||
BatchTable *pTab;
|
||||
BatchTable *pTabNext;
|
||||
|
||||
sqlite3changebatch_zero(p);
|
||||
for(pTab=p->pTab; pTab; pTab=pTabNext){
|
||||
BatchIndex *pIdx;
|
||||
BatchIndex *pIdxNext;
|
||||
for(pIdx=pTab->pIdx; pIdx; pIdx=pIdxNext){
|
||||
pIdxNext = pIdx->pNext;
|
||||
cbFree(pIdx);
|
||||
}
|
||||
pTabNext = pTab->pNext;
|
||||
cbFree(pTab);
|
||||
}
|
||||
cbFree(p);
|
||||
}
|
||||
|
||||
#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
|
Reference in New Issue
Block a user