SQLite

Check-in [0c9fd6b723]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add an experimental module to detect conflicts between sessions changesets.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | changebatch
Files: files | file ages | folders
SHA1: 0c9fd6b723041955b5182caa430312e5124fdc83
User & Date: dan 2016-08-22 20:49:06.286
Context
2016-08-22
21:01
Add a couple of extra tests to changebatch1.test. (check-in: 207d970b79 user: dan tags: changebatch)
20:49
Add an experimental module to detect conflicts between sessions changesets. (check-in: 0c9fd6b723 user: dan tags: changebatch)
2016-08-19
15:15
Enhance the VACUUM command so that it can operate on an attached database. (check-in: 083f9e6270 user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Added ext/session/changebatch1.test.














































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 2016 August 23
#
# 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 regression tests for SQLite library.
#

if {![info exists testdir]} {
  set testdir [file join [file dirname [info script]] .. .. test]
} 
source $testdir/tester.tcl
ifcapable !session {finish_test; return}

set testprefix changebatch1

proc do_changebatch_test {tn args} {
  set C [list]
  foreach a $args {
    sqlite3session S db main
    S attach *
    execsql $a
    lappend C [S changeset]
    S delete
  }

  sqlite3changebatch cb db
  set i 1
  foreach ::cs [lrange $C 0 end-1] {
    do_test $tn.$i { cb add [set ::cs] } SQLITE_OK
    incr i
  }

  set ::cs [lindex $C end]
  do_test $tn.$i { cb add [set ::cs] } SQLITE_CONSTRAINT

  cb delete
}

do_execsql_test 1.0 {
  CREATE TABLE t1(a PRIMARY KEY, b);
}

do_changebatch_test 1.1 {
  INSERT INTO t1 VALUES(1, 1);
} {
  DELETE FROM t1 WHERE a=1;
}

finish_test
Added ext/session/sqlite3changebatch.c.




















































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
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 */
Added ext/session/sqlite3changebatch.h.






















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

#if !defined(SQLITECHANGEBATCH_H_) 
#define SQLITECHANGEBATCH_H_ 1

typedef struct sqlite3_changebatch sqlite3_changebatch;

/*
** Create a new changebatch object for detecting conflicts between
** changesets associated with a schema equivalent to that of the "main"
** database of the open database handle db passed as the first
** parameter. It is the responsibility of the caller to ensure that
** the database handle is not closed until after the changebatch
** object has been deleted.
**
** A changebatch object is used to detect batches of non-conflicting
** changesets. Changesets that do not conflict may be applied to the 
** target database in any order without affecting the final state of 
** the database.
**
** The changebatch object only works reliably if PRIMARY KEY and UNIQUE
** constraints on tables affected by the changesets use collation
** sequences that are equivalent to built-in collation sequence 
** BINARY for the == operation.
**
** If successful, SQLITE_OK is returned and (*pp) set to point to
** the new changebatch object. If an error occurs, an SQLite error
** code is returned and the final value of (*pp) is undefined.
*/
int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp);

/*
** Argument p points to a buffer containing a changeset n bytes in
** size. Assuming no error occurs, this function returns SQLITE_OK
** if the changeset does not conflict with any changeset passed 
** to an sqlite3changebatch_add() call made on the same 
** sqlite3_changebatch* handle since the most recent call to
** sqlite3changebatch_zero(). If the changeset does conflict with 
** an earlier such changeset, SQLITE_CONSTRAINT is returned. Or, 
** if an error occurs, some other SQLite error code may be returned.
**
** One changeset is said to conflict with another if
** either:
**
**   * the two changesets contain operations (INSERT, UPDATE or 
**     DELETE) on the same row, identified by primary key, or
**
**   * the two changesets contain operations (INSERT, UPDATE or 
**     DELETE) on rows with identical values in any combination 
**     of fields constrained by a UNIQUE constraint.
**
** Even if this function returns SQLITE_CONFLICT, the current
** changeset is added to the internal data structures - so future
** calls to this function may conflict with it. If this function
** returns any result code other than SQLITE_OK or SQLITE_CONFLICT,
** the result of any future call to sqlite3changebatch_add() is
** undefined.
**
** Only changesets may be passed to this function. Passing a 
** patchset to this function results in an SQLITE_MISUSE error.
*/
int sqlite3changebatch_add(sqlite3_changebatch*, void *p, int n);

/*
** Zero a changebatch object. This causes the records of all earlier 
** calls to sqlite3changebatch_add() to be discarded.
*/
void sqlite3changebatch_zero(sqlite3_changebatch*);

/*
** Delete a changebatch object.
*/
void sqlite3changebatch_delete(sqlite3_changebatch*);

#endif /* !defined(SQLITECHANGEBATCH_H_) */

Changes to ext/session/test_session.c.
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
   || TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res)
  ){
    Tcl_BackgroundError(interp);
  }

  Tcl_DecrRefCount(pEval);
  return res;
}  

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;







|







369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
   || TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res)
  ){
    Tcl_BackgroundError(interp);
  }

  Tcl_DecrRefCount(pEval);
  return res;
}

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;
913
914
915
916
917
918
919

























































































































920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938




939
940
941
942
  }
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  return TCL_OK;
}


























































































































int TestSession_Init(Tcl_Interp *interp){
  Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
  Tcl_CreateObjCommand(
      interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_apply_replace_all", 
      test_sqlite3changeset_apply_replace_all, 0, 0
  );




  return TCL_OK;
}

#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



















>
>
>
>




913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
  }
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  return TCL_OK;
}

#include "sqlite3changebatch.h"

typedef struct TestChangebatch TestChangebatch;
struct TestChangebatch {
  sqlite3_changebatch *pChangebatch;
};

/*
** Tclcmd:  $changebatch add BLOB
**          $changebatch zero
**          $changebatch delete
*/
static int SQLITE_TCLAPI test_changebatch_cmd(
  void *clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
  TestChangebatch *p = (TestChangebatch*)clientData;
  sqlite3_changebatch *pChangebatch = p->pChangebatch;
  struct SessionSubcmd {
    const char *zSub;
    int nArg;
    const char *zMsg;
    int iSub;
  } aSub[] = {
    { "add",          1, "CHANGESET",  }, /* 0 */
    { "zero",         0, "",           }, /* 1 */
    { "delete",       0, "",           }, /* 2 */
    { 0 }
  };
  int iSub;
  int rc;

  if( objc<2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
    return TCL_ERROR;
  }
  rc = Tcl_GetIndexFromObjStruct(interp, 
      objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
  );
  if( rc!=TCL_OK ) return rc;
  if( objc!=2+aSub[iSub].nArg ){
    Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
    return TCL_ERROR;
  }

  switch( iSub ){
    case 0: {      /* add */
      int nArg;
      unsigned char *pArg = Tcl_GetByteArrayFromObj(objv[2], &nArg);
      rc = sqlite3changebatch_add(pChangebatch, pArg, nArg);
      if( rc!=SQLITE_OK && rc!=SQLITE_CONSTRAINT ){
        return test_session_error(interp, rc, 0);
      }else{
        extern const char *sqlite3ErrName(int);
        Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
      }
      break;
    }

    case 1: {      /* zero */
      sqlite3changebatch_zero(pChangebatch);
      break;
    }

    case 2:        /* delete */
      Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
      break;
  }

  return TCL_OK;
}

static void SQLITE_TCLAPI test_changebatch_del(void *clientData){
  TestChangebatch *p = (TestChangebatch*)clientData;
  sqlite3changebatch_delete(p->pChangebatch);
  ckfree((char*)p);
}

/*
** Tclcmd:  sqlite3changebatch CMD DB-HANDLE
*/
static int SQLITE_TCLAPI test_sqlite3changebatch(
  void * clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
  sqlite3 *db;
  Tcl_CmdInfo info;
  int rc;                         /* sqlite3session_create() return code */
  TestChangebatch *p;             /* New wrapper object */

  if( objc!=3 ){
    Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE");
    return TCL_ERROR;
  }

  if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){
    Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
    return TCL_ERROR;
  }
  db = *(sqlite3 **)info.objClientData;

  p = (TestChangebatch*)ckalloc(sizeof(TestChangebatch));
  memset(p, 0, sizeof(TestChangebatch));
  rc = sqlite3changebatch_new(db, &p->pChangebatch);
  if( rc!=SQLITE_OK ){
    ckfree((char*)p);
    return test_session_error(interp, rc, 0);
  }

  Tcl_CreateObjCommand(
      interp, Tcl_GetString(objv[1]), test_changebatch_cmd, (ClientData)p,
      test_changebatch_del
  );
  Tcl_SetObjResult(interp, objv[1]);
  return TCL_OK;
}

int TestSession_Init(Tcl_Interp *interp){
  Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
  Tcl_CreateObjCommand(
      interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
  );
  Tcl_CreateObjCommand(
      interp, "sqlite3changeset_apply_replace_all", 
      test_sqlite3changeset_apply_replace_all, 0, 0
  );

  Tcl_CreateObjCommand(
      interp, "sqlite3changebatch", test_sqlite3changebatch, 0, 0
  );
  return TCL_OK;
}

#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */
Changes to main.mk.
386
387
388
389
390
391
392

393
394
395
396
397
398
399
  $(TOP)/ext/fts3/fts3.c \
  $(TOP)/ext/fts3/fts3_aux.c \
  $(TOP)/ext/fts3/fts3_expr.c \
  $(TOP)/ext/fts3/fts3_tokenizer.c \
  $(TOP)/ext/fts3/fts3_write.c \
  $(TOP)/ext/async/sqlite3async.c \
  $(TOP)/ext/session/sqlite3session.c \

  $(TOP)/ext/session/test_session.c 

# Header files used by all library source files.
#
HDR = \
   $(TOP)/src/btree.h \
   $(TOP)/src/btreeInt.h \







>







386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
  $(TOP)/ext/fts3/fts3.c \
  $(TOP)/ext/fts3/fts3_aux.c \
  $(TOP)/ext/fts3/fts3_expr.c \
  $(TOP)/ext/fts3/fts3_tokenizer.c \
  $(TOP)/ext/fts3/fts3_write.c \
  $(TOP)/ext/async/sqlite3async.c \
  $(TOP)/ext/session/sqlite3session.c \
  $(TOP)/ext/session/sqlite3changebatch.c \
  $(TOP)/ext/session/test_session.c 

# Header files used by all library source files.
#
HDR = \
   $(TOP)/src/btree.h \
   $(TOP)/src/btreeInt.h \