xref: /sqlite-3.40.0/ext/session/test_session.c (revision aeb4e6ee)
1 
2 #if defined(SQLITE_TEST) && defined(SQLITE_ENABLE_SESSION) \
3  && defined(SQLITE_ENABLE_PREUPDATE_HOOK)
4 
5 #include "sqlite3session.h"
6 #include <assert.h>
7 #include <string.h>
8 #if defined(INCLUDE_SQLITE_TCL_H)
9 #  include "sqlite_tcl.h"
10 #else
11 #  include "tcl.h"
12 #  ifndef SQLITE_TCLAPI
13 #    define SQLITE_TCLAPI
14 #  endif
15 #endif
16 
17 #ifndef SQLITE_AMALGAMATION
18   typedef unsigned char u8;
19 #endif
20 
21 typedef struct TestSession TestSession;
22 struct TestSession {
23   sqlite3_session *pSession;
24   Tcl_Interp *interp;
25   Tcl_Obj *pFilterScript;
26 };
27 
28 typedef struct TestStreamInput TestStreamInput;
29 struct TestStreamInput {
30   int nStream;                    /* Maximum chunk size */
31   unsigned char *aData;           /* Pointer to buffer containing data */
32   int nData;                      /* Size of buffer aData in bytes */
33   int iData;                      /* Bytes of data already read by sessions */
34 };
35 
36 /*
37 ** Extract an sqlite3* db handle from the object passed as the second
38 ** argument. If successful, set *pDb to point to the db handle and return
39 ** TCL_OK. Otherwise, return TCL_ERROR.
40 */
41 static int dbHandleFromObj(Tcl_Interp *interp, Tcl_Obj *pObj, sqlite3 **pDb){
42   Tcl_CmdInfo info;
43   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(pObj), &info) ){
44     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(pObj), 0);
45     return TCL_ERROR;
46   }
47 
48   *pDb = *(sqlite3 **)info.objClientData;
49   return TCL_OK;
50 }
51 
52 /*************************************************************************
53 ** The following code is copied byte-for-byte from the sessions module
54 ** documentation.  It is used by some of the sessions modules tests to
55 ** ensure that the example in the documentation does actually work.
56 */
57 /*
58 ** Argument zSql points to a buffer containing an SQL script to execute
59 ** against the database handle passed as the first argument. As well as
60 ** executing the SQL script, this function collects a changeset recording
61 ** all changes made to the "main" database file. Assuming no error occurs,
62 ** output variables (*ppChangeset) and (*pnChangeset) are set to point
63 ** to a buffer containing the changeset and the size of the changeset in
64 ** bytes before returning SQLITE_OK. In this case it is the responsibility
65 ** of the caller to eventually free the changeset blob by passing it to
66 ** the sqlite3_free function.
67 **
68 ** Or, if an error does occur, return an SQLite error code. The final
69 ** value of (*pChangeset) and (*pnChangeset) are undefined in this case.
70 */
71 int sql_exec_changeset(
72   sqlite3 *db,                  /* Database handle */
73   const char *zSql,             /* SQL script to execute */
74   int *pnChangeset,             /* OUT: Size of changeset blob in bytes */
75   void **ppChangeset            /* OUT: Pointer to changeset blob */
76 ){
77   sqlite3_session *pSession = 0;
78   int rc;
79 
80   /* Create a new session object */
81   rc = sqlite3session_create(db, "main", &pSession);
82 
83   /* Configure the session object to record changes to all tables */
84   if( rc==SQLITE_OK ) rc = sqlite3session_attach(pSession, NULL);
85 
86   /* Execute the SQL script */
87   if( rc==SQLITE_OK ) rc = sqlite3_exec(db, zSql, 0, 0, 0);
88 
89   /* Collect the changeset */
90   if( rc==SQLITE_OK ){
91     rc = sqlite3session_changeset(pSession, pnChangeset, ppChangeset);
92   }
93 
94   /* Delete the session object */
95   sqlite3session_delete(pSession);
96 
97   return rc;
98 }
99 /************************************************************************/
100 
101 /*
102 ** Tclcmd: sql_exec_changeset DB SQL
103 */
104 static int SQLITE_TCLAPI test_sql_exec_changeset(
105   void * clientData,
106   Tcl_Interp *interp,
107   int objc,
108   Tcl_Obj *CONST objv[]
109 ){
110   const char *zSql;
111   sqlite3 *db;
112   void *pChangeset;
113   int nChangeset;
114   int rc;
115 
116   if( objc!=3 ){
117     Tcl_WrongNumArgs(interp, 1, objv, "DB SQL");
118     return TCL_ERROR;
119   }
120   if( dbHandleFromObj(interp, objv[1], &db) ) return TCL_ERROR;
121   zSql = (const char*)Tcl_GetString(objv[2]);
122 
123   rc = sql_exec_changeset(db, zSql, &nChangeset, &pChangeset);
124   if( rc!=SQLITE_OK ){
125     Tcl_ResetResult(interp);
126     Tcl_AppendResult(interp, "error in sql_exec_changeset()", 0);
127     return TCL_ERROR;
128   }
129 
130   Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChangeset, nChangeset));
131   sqlite3_free(pChangeset);
132   return TCL_OK;
133 }
134 
135 
136 
137 #define SESSION_STREAM_TCL_VAR "sqlite3session_streams"
138 
139 /*
140 ** Attempt to find the global variable zVar within interpreter interp
141 ** and extract an integer value from it. Return this value.
142 **
143 ** If the named variable cannot be found, or if it cannot be interpreted
144 ** as a integer, return 0.
145 */
146 static int test_tcl_integer(Tcl_Interp *interp, const char *zVar){
147   Tcl_Obj *pObj;
148   int iVal = 0;
149   pObj = Tcl_ObjGetVar2(interp, Tcl_NewStringObj(zVar, -1), 0, TCL_GLOBAL_ONLY);
150   if( pObj ) Tcl_GetIntFromObj(0, pObj, &iVal);
151   return iVal;
152 }
153 
154 static int test_session_error(Tcl_Interp *interp, int rc, char *zErr){
155   extern const char *sqlite3ErrName(int);
156   Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
157   if( zErr ){
158     Tcl_AppendResult(interp, " - ", zErr, 0);
159     sqlite3_free(zErr);
160   }
161   return TCL_ERROR;
162 }
163 
164 static int test_table_filter(void *pCtx, const char *zTbl){
165   TestSession *p = (TestSession*)pCtx;
166   Tcl_Obj *pEval;
167   int rc;
168   int bRes = 0;
169 
170   pEval = Tcl_DuplicateObj(p->pFilterScript);
171   Tcl_IncrRefCount(pEval);
172   rc = Tcl_ListObjAppendElement(p->interp, pEval, Tcl_NewStringObj(zTbl, -1));
173   if( rc==TCL_OK ){
174     rc = Tcl_EvalObjEx(p->interp, pEval, TCL_EVAL_GLOBAL);
175   }
176   if( rc==TCL_OK ){
177     rc = Tcl_GetBooleanFromObj(p->interp, Tcl_GetObjResult(p->interp), &bRes);
178   }
179   if( rc!=TCL_OK ){
180     /* printf("error: %s\n", Tcl_GetStringResult(p->interp)); */
181     Tcl_BackgroundError(p->interp);
182   }
183   Tcl_DecrRefCount(pEval);
184 
185   return bRes;
186 }
187 
188 struct TestSessionsBlob {
189   void *p;
190   int n;
191 };
192 typedef struct TestSessionsBlob TestSessionsBlob;
193 
194 static int testStreamOutput(
195   void *pCtx,
196   const void *pData,
197   int nData
198 ){
199   TestSessionsBlob *pBlob = (TestSessionsBlob*)pCtx;
200   char *pNew;
201 
202   assert( nData>0 );
203   pNew = (char*)sqlite3_realloc(pBlob->p, pBlob->n + nData);
204   if( pNew==0 ){
205     return SQLITE_NOMEM;
206   }
207   pBlob->p = (void*)pNew;
208   memcpy(&pNew[pBlob->n], pData, nData);
209   pBlob->n += nData;
210   return SQLITE_OK;
211 }
212 
213 /*
214 ** Tclcmd:  $session attach TABLE
215 **          $session changeset
216 **          $session delete
217 **          $session enable BOOL
218 **          $session indirect INTEGER
219 **          $session patchset
220 **          $session table_filter SCRIPT
221 */
222 static int SQLITE_TCLAPI test_session_cmd(
223   void *clientData,
224   Tcl_Interp *interp,
225   int objc,
226   Tcl_Obj *CONST objv[]
227 ){
228   TestSession *p = (TestSession*)clientData;
229   sqlite3_session *pSession = p->pSession;
230   static struct SessionSubcmd {
231     const char *zSub;
232     int nArg;
233     const char *zMsg;
234     int iSub;
235   } aSub[] = {
236     { "attach",       1, "TABLE",      }, /* 0 */
237     { "changeset",    0, "",           }, /* 1 */
238     { "delete",       0, "",           }, /* 2 */
239     { "enable",       1, "BOOL",       }, /* 3 */
240     { "indirect",     1, "BOOL",       }, /* 4 */
241     { "isempty",      0, "",           }, /* 5 */
242     { "table_filter", 1, "SCRIPT",     }, /* 6 */
243     { "patchset",     0, "",           }, /* 7 */
244     { "diff",         2, "FROMDB TBL", }, /* 8 */
245     { 0 }
246   };
247   int iSub;
248   int rc;
249 
250   if( objc<2 ){
251     Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
252     return TCL_ERROR;
253   }
254   rc = Tcl_GetIndexFromObjStruct(interp,
255       objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
256   );
257   if( rc!=TCL_OK ) return rc;
258   if( objc!=2+aSub[iSub].nArg ){
259     Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
260     return TCL_ERROR;
261   }
262 
263   switch( iSub ){
264     case 0: {      /* attach */
265       char *zArg = Tcl_GetString(objv[2]);
266       if( zArg[0]=='*' && zArg[1]=='\0' ) zArg = 0;
267       rc = sqlite3session_attach(pSession, zArg);
268       if( rc!=SQLITE_OK ){
269         return test_session_error(interp, rc, 0);
270       }
271       break;
272     }
273 
274     case 7:        /* patchset */
275     case 1: {      /* changeset */
276       TestSessionsBlob o = {0, 0};
277       if( test_tcl_integer(interp, SESSION_STREAM_TCL_VAR) ){
278         void *pCtx = (void*)&o;
279         if( iSub==7 ){
280           rc = sqlite3session_patchset_strm(pSession, testStreamOutput, pCtx);
281         }else{
282           rc = sqlite3session_changeset_strm(pSession, testStreamOutput, pCtx);
283         }
284       }else{
285         if( iSub==7 ){
286           rc = sqlite3session_patchset(pSession, &o.n, &o.p);
287         }else{
288           rc = sqlite3session_changeset(pSession, &o.n, &o.p);
289         }
290       }
291       if( rc==SQLITE_OK ){
292         Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(o.p, o.n));
293       }
294       sqlite3_free(o.p);
295       if( rc!=SQLITE_OK ){
296         return test_session_error(interp, rc, 0);
297       }
298       break;
299     }
300 
301     case 2:        /* delete */
302       Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
303       break;
304 
305     case 3: {      /* enable */
306       int val;
307       if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
308       val = sqlite3session_enable(pSession, val);
309       Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
310       break;
311     }
312 
313     case 4: {      /* indirect */
314       int val;
315       if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
316       val = sqlite3session_indirect(pSession, val);
317       Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
318       break;
319     }
320 
321     case 5: {      /* isempty */
322       int val;
323       val = sqlite3session_isempty(pSession);
324       Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
325       break;
326     }
327 
328     case 6: {      /* table_filter */
329       if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript);
330       p->interp = interp;
331       p->pFilterScript = Tcl_DuplicateObj(objv[2]);
332       Tcl_IncrRefCount(p->pFilterScript);
333       sqlite3session_table_filter(pSession, test_table_filter, clientData);
334       break;
335     }
336 
337     case 8: {      /* diff */
338       char *zErr = 0;
339       rc = sqlite3session_diff(pSession,
340           Tcl_GetString(objv[2]),
341           Tcl_GetString(objv[3]),
342           &zErr
343       );
344       assert( rc!=SQLITE_OK || zErr==0 );
345       if( rc ){
346         return test_session_error(interp, rc, zErr);
347       }
348       break;
349     }
350   }
351 
352   return TCL_OK;
353 }
354 
355 static void SQLITE_TCLAPI test_session_del(void *clientData){
356   TestSession *p = (TestSession*)clientData;
357   if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript);
358   sqlite3session_delete(p->pSession);
359   ckfree((char*)p);
360 }
361 
362 /*
363 ** Tclcmd:  sqlite3session CMD DB-HANDLE DB-NAME
364 */
365 static int SQLITE_TCLAPI test_sqlite3session(
366   void * clientData,
367   Tcl_Interp *interp,
368   int objc,
369   Tcl_Obj *CONST objv[]
370 ){
371   sqlite3 *db;
372   Tcl_CmdInfo info;
373   int rc;                         /* sqlite3session_create() return code */
374   TestSession *p;                 /* New wrapper object */
375 
376   if( objc!=4 ){
377     Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE DB-NAME");
378     return TCL_ERROR;
379   }
380 
381   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){
382     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
383     return TCL_ERROR;
384   }
385   db = *(sqlite3 **)info.objClientData;
386 
387   p = (TestSession*)ckalloc(sizeof(TestSession));
388   memset(p, 0, sizeof(TestSession));
389   rc = sqlite3session_create(db, Tcl_GetString(objv[3]), &p->pSession);
390   if( rc!=SQLITE_OK ){
391     ckfree((char*)p);
392     return test_session_error(interp, rc, 0);
393   }
394 
395   Tcl_CreateObjCommand(
396       interp, Tcl_GetString(objv[1]), test_session_cmd, (ClientData)p,
397       test_session_del
398   );
399   Tcl_SetObjResult(interp, objv[1]);
400   return TCL_OK;
401 }
402 
403 static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){
404   if( pVal==0 ){
405     Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
406     Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
407   }else{
408     Tcl_Obj *pObj;
409     switch( sqlite3_value_type(pVal) ){
410       case SQLITE_NULL:
411         Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("n", 1));
412         pObj = Tcl_NewObj();
413         break;
414       case SQLITE_INTEGER:
415         Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("i", 1));
416         pObj = Tcl_NewWideIntObj(sqlite3_value_int64(pVal));
417         break;
418       case SQLITE_FLOAT:
419         Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("f", 1));
420         pObj = Tcl_NewDoubleObj(sqlite3_value_double(pVal));
421         break;
422       case SQLITE_TEXT: {
423         const char *z = (char*)sqlite3_value_blob(pVal);
424         int n = sqlite3_value_bytes(pVal);
425         Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("t", 1));
426         pObj = Tcl_NewStringObj(z, n);
427         break;
428       }
429       default:
430         assert( sqlite3_value_type(pVal)==SQLITE_BLOB );
431         Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("b", 1));
432         pObj = Tcl_NewByteArrayObj(
433             sqlite3_value_blob(pVal),
434             sqlite3_value_bytes(pVal)
435         );
436         break;
437     }
438     Tcl_ListObjAppendElement(0, pList, pObj);
439   }
440 }
441 
442 typedef struct TestConflictHandler TestConflictHandler;
443 struct TestConflictHandler {
444   Tcl_Interp *interp;
445   Tcl_Obj *pConflictScript;
446   Tcl_Obj *pFilterScript;
447 };
448 
449 static int test_obj_eq_string(Tcl_Obj *p, const char *z){
450   int n;
451   int nObj;
452   char *zObj;
453 
454   n = (int)strlen(z);
455   zObj = Tcl_GetStringFromObj(p, &nObj);
456 
457   return (nObj==n && (n==0 || 0==memcmp(zObj, z, n)));
458 }
459 
460 static int test_filter_handler(
461   void *pCtx,                     /* Pointer to TestConflictHandler structure */
462   const char *zTab                /* Table name */
463 ){
464   TestConflictHandler *p = (TestConflictHandler *)pCtx;
465   int res = 1;
466   Tcl_Obj *pEval;
467   Tcl_Interp *interp = p->interp;
468 
469   pEval = Tcl_DuplicateObj(p->pFilterScript);
470   Tcl_IncrRefCount(pEval);
471 
472   if( TCL_OK!=Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1))
473    || TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL)
474    || TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res)
475   ){
476     Tcl_BackgroundError(interp);
477   }
478 
479   Tcl_DecrRefCount(pEval);
480   return res;
481 }
482 
483 static int test_conflict_handler(
484   void *pCtx,                     /* Pointer to TestConflictHandler structure */
485   int eConf,                      /* DATA, MISSING, CONFLICT, CONSTRAINT */
486   sqlite3_changeset_iter *pIter   /* Handle describing change and conflict */
487 ){
488   TestConflictHandler *p = (TestConflictHandler *)pCtx;
489   Tcl_Obj *pEval;
490   Tcl_Interp *interp = p->interp;
491   int ret = 0;                    /* Return value */
492 
493   int op;                         /* SQLITE_UPDATE, DELETE or INSERT */
494   const char *zTab;               /* Name of table conflict is on */
495   int nCol;                       /* Number of columns in table zTab */
496 
497   pEval = Tcl_DuplicateObj(p->pConflictScript);
498   Tcl_IncrRefCount(pEval);
499 
500   sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
501 
502   if( eConf==SQLITE_CHANGESET_FOREIGN_KEY ){
503     int nFk;
504     sqlite3changeset_fk_conflicts(pIter, &nFk);
505     Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj("FOREIGN_KEY", -1));
506     Tcl_ListObjAppendElement(0, pEval, Tcl_NewIntObj(nFk));
507   }else{
508 
509     /* Append the operation type. */
510     Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(
511         op==SQLITE_INSERT ? "INSERT" :
512         op==SQLITE_UPDATE ? "UPDATE" :
513         "DELETE", -1
514     ));
515 
516     /* Append the table name. */
517     Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1));
518 
519     /* Append the conflict type. */
520     switch( eConf ){
521       case SQLITE_CHANGESET_DATA:
522         Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1));
523         break;
524       case SQLITE_CHANGESET_NOTFOUND:
525         Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1));
526         break;
527       case SQLITE_CHANGESET_CONFLICT:
528         Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1));
529         break;
530       case SQLITE_CHANGESET_CONSTRAINT:
531         Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1));
532         break;
533     }
534 
535     /* If this is not an INSERT, append the old row */
536     if( op!=SQLITE_INSERT ){
537       int i;
538       Tcl_Obj *pOld = Tcl_NewObj();
539       for(i=0; i<nCol; i++){
540         sqlite3_value *pVal;
541         sqlite3changeset_old(pIter, i, &pVal);
542         test_append_value(pOld, pVal);
543       }
544       Tcl_ListObjAppendElement(0, pEval, pOld);
545     }
546 
547     /* If this is not a DELETE, append the new row */
548     if( op!=SQLITE_DELETE ){
549       int i;
550       Tcl_Obj *pNew = Tcl_NewObj();
551       for(i=0; i<nCol; i++){
552         sqlite3_value *pVal;
553         sqlite3changeset_new(pIter, i, &pVal);
554         test_append_value(pNew, pVal);
555       }
556       Tcl_ListObjAppendElement(0, pEval, pNew);
557     }
558 
559     /* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append
560      ** the conflicting row.  */
561     if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){
562       int i;
563       Tcl_Obj *pConflict = Tcl_NewObj();
564       for(i=0; i<nCol; i++){
565         int rc;
566         sqlite3_value *pVal;
567         rc = sqlite3changeset_conflict(pIter, i, &pVal);
568         assert( rc==SQLITE_OK );
569         test_append_value(pConflict, pVal);
570       }
571       Tcl_ListObjAppendElement(0, pEval, pConflict);
572     }
573 
574     /***********************************************************************
575      ** This block is purely for testing some error conditions.
576      */
577     if( eConf==SQLITE_CHANGESET_CONSTRAINT
578      || eConf==SQLITE_CHANGESET_NOTFOUND
579     ){
580       sqlite3_value *pVal;
581       int rc = sqlite3changeset_conflict(pIter, 0, &pVal);
582       assert( rc==SQLITE_MISUSE );
583     }else{
584       sqlite3_value *pVal;
585       int rc = sqlite3changeset_conflict(pIter, -1, &pVal);
586       assert( rc==SQLITE_RANGE );
587       rc = sqlite3changeset_conflict(pIter, nCol, &pVal);
588       assert( rc==SQLITE_RANGE );
589     }
590     if( op==SQLITE_DELETE ){
591       sqlite3_value *pVal;
592       int rc = sqlite3changeset_new(pIter, 0, &pVal);
593       assert( rc==SQLITE_MISUSE );
594     }else{
595       sqlite3_value *pVal;
596       int rc = sqlite3changeset_new(pIter, -1, &pVal);
597       assert( rc==SQLITE_RANGE );
598       rc = sqlite3changeset_new(pIter, nCol, &pVal);
599       assert( rc==SQLITE_RANGE );
600     }
601     if( op==SQLITE_INSERT ){
602       sqlite3_value *pVal;
603       int rc = sqlite3changeset_old(pIter, 0, &pVal);
604       assert( rc==SQLITE_MISUSE );
605     }else{
606       sqlite3_value *pVal;
607       int rc = sqlite3changeset_old(pIter, -1, &pVal);
608       assert( rc==SQLITE_RANGE );
609       rc = sqlite3changeset_old(pIter, nCol, &pVal);
610       assert( rc==SQLITE_RANGE );
611     }
612     if( eConf!=SQLITE_CHANGESET_FOREIGN_KEY ){
613       /* eConf!=FOREIGN_KEY is always true at this point. The condition is
614       ** just there to make it clearer what is being tested.  */
615       int nDummy;
616       int rc = sqlite3changeset_fk_conflicts(pIter, &nDummy);
617       assert( rc==SQLITE_MISUSE );
618     }
619     /* End of testing block
620     ***********************************************************************/
621   }
622 
623   if( TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) ){
624     Tcl_BackgroundError(interp);
625   }else{
626     Tcl_Obj *pRes = Tcl_GetObjResult(interp);
627     if( test_obj_eq_string(pRes, "OMIT") || test_obj_eq_string(pRes, "") ){
628       ret = SQLITE_CHANGESET_OMIT;
629     }else if( test_obj_eq_string(pRes, "REPLACE") ){
630       ret = SQLITE_CHANGESET_REPLACE;
631     }else if( test_obj_eq_string(pRes, "ABORT") ){
632       ret = SQLITE_CHANGESET_ABORT;
633     }else{
634       Tcl_GetIntFromObj(0, pRes, &ret);
635     }
636   }
637 
638   Tcl_DecrRefCount(pEval);
639   return ret;
640 }
641 
642 /*
643 ** The conflict handler used by sqlite3changeset_apply_replace_all().
644 ** This conflict handler calls sqlite3_value_text16() on all available
645 ** sqlite3_value objects and then returns CHANGESET_REPLACE, or
646 ** CHANGESET_OMIT if REPLACE is not applicable. This is used to test the
647 ** effect of a malloc failure within an sqlite3_value_xxx() function
648 ** invoked by a conflict-handler callback.
649 */
650 static int replace_handler(
651   void *pCtx,                     /* Pointer to TestConflictHandler structure */
652   int eConf,                      /* DATA, MISSING, CONFLICT, CONSTRAINT */
653   sqlite3_changeset_iter *pIter   /* Handle describing change and conflict */
654 ){
655   int op;                         /* SQLITE_UPDATE, DELETE or INSERT */
656   const char *zTab;               /* Name of table conflict is on */
657   int nCol;                       /* Number of columns in table zTab */
658   int i;
659   int x = 0;
660 
661   sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
662 
663   if( op!=SQLITE_INSERT ){
664     for(i=0; i<nCol; i++){
665       sqlite3_value *pVal;
666       sqlite3changeset_old(pIter, i, &pVal);
667       sqlite3_value_text16(pVal);
668       x++;
669     }
670   }
671 
672   if( op!=SQLITE_DELETE ){
673     for(i=0; i<nCol; i++){
674       sqlite3_value *pVal;
675       sqlite3changeset_new(pIter, i, &pVal);
676       sqlite3_value_text16(pVal);
677       x++;
678     }
679   }
680 
681   if( eConf==SQLITE_CHANGESET_DATA ){
682     return SQLITE_CHANGESET_REPLACE;
683   }
684   return SQLITE_CHANGESET_OMIT;
685 }
686 
687 static int testStreamInput(
688   void *pCtx,                     /* Context pointer */
689   void *pData,                    /* Buffer to populate */
690   int *pnData                     /* IN/OUT: Bytes requested/supplied */
691 ){
692   TestStreamInput *p = (TestStreamInput*)pCtx;
693   int nReq = *pnData;             /* Bytes of data requested */
694   int nRem = p->nData - p->iData; /* Bytes of data available */
695   int nRet = p->nStream;          /* Bytes actually returned */
696 
697   /* Allocate and free some space. There is no point to this, other than
698   ** that it allows the regular OOM fault-injection tests to cause an error
699   ** in this function.  */
700   void *pAlloc = sqlite3_malloc(10);
701   if( pAlloc==0 ) return SQLITE_NOMEM;
702   sqlite3_free(pAlloc);
703 
704   if( nRet>nReq ) nRet = nReq;
705   if( nRet>nRem ) nRet = nRem;
706 
707   assert( nRet>=0 );
708   if( nRet>0 ){
709     memcpy(pData, &p->aData[p->iData], nRet);
710     p->iData += nRet;
711   }
712 
713   *pnData = nRet;
714   return SQLITE_OK;
715 }
716 
717 
718 static int SQLITE_TCLAPI testSqlite3changesetApply(
719   int bV2,
720   void * clientData,
721   Tcl_Interp *interp,
722   int objc,
723   Tcl_Obj *CONST objv[]
724 ){
725   sqlite3 *db;                    /* Database handle */
726   Tcl_CmdInfo info;               /* Database Tcl command (objv[1]) info */
727   int rc;                         /* Return code from changeset_invert() */
728   void *pChangeset;               /* Buffer containing changeset */
729   int nChangeset;                 /* Size of buffer aChangeset in bytes */
730   TestConflictHandler ctx;
731   TestStreamInput sStr;
732   void *pRebase = 0;
733   int nRebase = 0;
734   int flags = 0;                  /* Flags for apply_v2() */
735 
736   memset(&sStr, 0, sizeof(sStr));
737   sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
738 
739   /* Check for the -nosavepoint flag */
740   if( bV2 ){
741     if( objc>1 ){
742       const char *z1 = Tcl_GetString(objv[1]);
743       int n = strlen(z1);
744       if( n>1 && n<=12 && 0==sqlite3_strnicmp("-nosavepoint", z1, n) ){
745         flags |= SQLITE_CHANGESETAPPLY_NOSAVEPOINT;
746         objc--;
747         objv++;
748       }
749     }
750     if( objc>1 ){
751       const char *z1 = Tcl_GetString(objv[1]);
752       int n = strlen(z1);
753       if( n>1 && n<=7 && 0==sqlite3_strnicmp("-invert", z1, n) ){
754         flags |= SQLITE_CHANGESETAPPLY_INVERT;
755         objc--;
756         objv++;
757       }
758     }
759   }
760 
761   if( objc!=4 && objc!=5 ){
762     const char *zMsg;
763     if( bV2 ){
764       zMsg = "?-nosavepoint? ?-inverse? "
765         "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
766     }else{
767       zMsg = "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
768     }
769     Tcl_WrongNumArgs(interp, 1, objv, zMsg);
770     return TCL_ERROR;
771   }
772   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
773     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[1]), 0);
774     return TCL_ERROR;
775   }
776   db = *(sqlite3 **)info.objClientData;
777   pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
778   ctx.pConflictScript = objv[3];
779   ctx.pFilterScript = objc==5 ? objv[4] : 0;
780   ctx.interp = interp;
781 
782   if( sStr.nStream==0 ){
783     if( bV2==0 ){
784       rc = sqlite3changeset_apply(db, nChangeset, pChangeset,
785           (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx
786       );
787     }else{
788       rc = sqlite3changeset_apply_v2(db, nChangeset, pChangeset,
789           (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx,
790           &pRebase, &nRebase, flags
791       );
792     }
793   }else{
794     sStr.aData = (unsigned char*)pChangeset;
795     sStr.nData = nChangeset;
796     if( bV2==0 ){
797       rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr,
798           (objc==5) ? test_filter_handler : 0,
799           test_conflict_handler, (void *)&ctx
800       );
801     }else{
802       rc = sqlite3changeset_apply_v2_strm(db, testStreamInput, (void*)&sStr,
803           (objc==5) ? test_filter_handler : 0,
804           test_conflict_handler, (void *)&ctx,
805           &pRebase, &nRebase, flags
806       );
807     }
808   }
809 
810   if( rc!=SQLITE_OK ){
811     return test_session_error(interp, rc, 0);
812   }else{
813     Tcl_ResetResult(interp);
814     if( bV2 && pRebase ){
815       Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pRebase, nRebase));
816     }
817   }
818   sqlite3_free(pRebase);
819   return TCL_OK;
820 }
821 
822 /*
823 ** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
824 */
825 static int SQLITE_TCLAPI test_sqlite3changeset_apply(
826   void * clientData,
827   Tcl_Interp *interp,
828   int objc,
829   Tcl_Obj *CONST objv[]
830 ){
831   return testSqlite3changesetApply(0, clientData, interp, objc, objv);
832 }
833 /*
834 ** sqlite3changeset_apply_v2 DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
835 */
836 static int SQLITE_TCLAPI test_sqlite3changeset_apply_v2(
837   void * clientData,
838   Tcl_Interp *interp,
839   int objc,
840   Tcl_Obj *CONST objv[]
841 ){
842   return testSqlite3changesetApply(1, clientData, interp, objc, objv);
843 }
844 
845 /*
846 ** sqlite3changeset_apply_replace_all DB CHANGESET
847 */
848 static int SQLITE_TCLAPI test_sqlite3changeset_apply_replace_all(
849   void * clientData,
850   Tcl_Interp *interp,
851   int objc,
852   Tcl_Obj *CONST objv[]
853 ){
854   sqlite3 *db;                    /* Database handle */
855   Tcl_CmdInfo info;               /* Database Tcl command (objv[1]) info */
856   int rc;                         /* Return code from changeset_invert() */
857   void *pChangeset;               /* Buffer containing changeset */
858   int nChangeset;                 /* Size of buffer aChangeset in bytes */
859 
860   if( objc!=3 ){
861     Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET");
862     return TCL_ERROR;
863   }
864   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
865     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
866     return TCL_ERROR;
867   }
868   db = *(sqlite3 **)info.objClientData;
869   pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
870 
871   rc = sqlite3changeset_apply(db, nChangeset, pChangeset, 0, replace_handler,0);
872   if( rc!=SQLITE_OK ){
873     return test_session_error(interp, rc, 0);
874   }
875   Tcl_ResetResult(interp);
876   return TCL_OK;
877 }
878 
879 
880 /*
881 ** sqlite3changeset_invert CHANGESET
882 */
883 static int SQLITE_TCLAPI test_sqlite3changeset_invert(
884   void * clientData,
885   Tcl_Interp *interp,
886   int objc,
887   Tcl_Obj *CONST objv[]
888 ){
889   int rc;                         /* Return code from changeset_invert() */
890   TestStreamInput sIn;            /* Input stream */
891   TestSessionsBlob sOut;          /* Output blob */
892 
893   if( objc!=2 ){
894     Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
895     return TCL_ERROR;
896   }
897 
898   memset(&sIn, 0, sizeof(sIn));
899   memset(&sOut, 0, sizeof(sOut));
900   sIn.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
901   sIn.aData = Tcl_GetByteArrayFromObj(objv[1], &sIn.nData);
902 
903   if( sIn.nStream ){
904     rc = sqlite3changeset_invert_strm(
905         testStreamInput, (void*)&sIn, testStreamOutput, (void*)&sOut
906     );
907   }else{
908     rc = sqlite3changeset_invert(sIn.nData, sIn.aData, &sOut.n, &sOut.p);
909   }
910   if( rc!=SQLITE_OK ){
911     rc = test_session_error(interp, rc, 0);
912   }else{
913     Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
914   }
915   sqlite3_free(sOut.p);
916   return rc;
917 }
918 
919 /*
920 ** sqlite3changeset_concat LEFT RIGHT
921 */
922 static int SQLITE_TCLAPI test_sqlite3changeset_concat(
923   void * clientData,
924   Tcl_Interp *interp,
925   int objc,
926   Tcl_Obj *CONST objv[]
927 ){
928   int rc;                         /* Return code from changeset_invert() */
929 
930   TestStreamInput sLeft;          /* Input stream */
931   TestStreamInput sRight;         /* Input stream */
932   TestSessionsBlob sOut = {0,0};  /* Output blob */
933 
934   if( objc!=3 ){
935     Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT");
936     return TCL_ERROR;
937   }
938 
939   memset(&sLeft, 0, sizeof(sLeft));
940   memset(&sRight, 0, sizeof(sRight));
941   sLeft.aData = Tcl_GetByteArrayFromObj(objv[1], &sLeft.nData);
942   sRight.aData = Tcl_GetByteArrayFromObj(objv[2], &sRight.nData);
943   sLeft.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
944   sRight.nStream = sLeft.nStream;
945 
946   if( sLeft.nStream>0 ){
947     rc = sqlite3changeset_concat_strm(
948         testStreamInput, (void*)&sLeft,
949         testStreamInput, (void*)&sRight,
950         testStreamOutput, (void*)&sOut
951     );
952   }else{
953     rc = sqlite3changeset_concat(
954         sLeft.nData, sLeft.aData, sRight.nData, sRight.aData, &sOut.n, &sOut.p
955     );
956   }
957 
958   if( rc!=SQLITE_OK ){
959     rc = test_session_error(interp, rc, 0);
960   }else{
961     Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
962   }
963   sqlite3_free(sOut.p);
964   return rc;
965 }
966 
967 /*
968 ** sqlite3session_foreach VARNAME CHANGESET SCRIPT
969 */
970 static int SQLITE_TCLAPI test_sqlite3session_foreach(
971   void * clientData,
972   Tcl_Interp *interp,
973   int objc,
974   Tcl_Obj *CONST objv[]
975 ){
976   void *pChangeset;
977   int nChangeset;
978   sqlite3_changeset_iter *pIter;
979   int rc;
980   Tcl_Obj *pVarname;
981   Tcl_Obj *pCS;
982   Tcl_Obj *pScript;
983   int isCheckNext = 0;
984   int isInvert = 0;
985 
986   TestStreamInput sStr;
987   memset(&sStr, 0, sizeof(sStr));
988 
989   while( objc>1 ){
990     char *zOpt = Tcl_GetString(objv[1]);
991     int nOpt = strlen(zOpt);
992     if( zOpt[0]!='-' ) break;
993     if( nOpt<=7 && 0==sqlite3_strnicmp(zOpt, "-invert", nOpt) ){
994       isInvert = 1;
995     }else
996     if( nOpt<=5 && 0==sqlite3_strnicmp(zOpt, "-next", nOpt) ){
997       isCheckNext = 1;
998     }else{
999       break;
1000     }
1001     objv++;
1002     objc--;
1003   }
1004   if( objc!=4 ){
1005     Tcl_WrongNumArgs(
1006         interp, 1, objv, "?-next? ?-invert? VARNAME CHANGESET SCRIPT");
1007     return TCL_ERROR;
1008   }
1009 
1010   pVarname = objv[1];
1011   pCS = objv[2];
1012   pScript = objv[3];
1013 
1014   pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset);
1015   sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
1016   if( isInvert ){
1017     int f = SQLITE_CHANGESETSTART_INVERT;
1018     if( sStr.nStream==0 ){
1019       rc = sqlite3changeset_start_v2(&pIter, nChangeset, pChangeset, f);
1020     }else{
1021       void *pCtx = (void*)&sStr;
1022       sStr.aData = (unsigned char*)pChangeset;
1023       sStr.nData = nChangeset;
1024       rc = sqlite3changeset_start_v2_strm(&pIter, testStreamInput, pCtx, f);
1025     }
1026   }else{
1027     if( sStr.nStream==0 ){
1028       rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
1029     }else{
1030       sStr.aData = (unsigned char*)pChangeset;
1031       sStr.nData = nChangeset;
1032       rc = sqlite3changeset_start_strm(&pIter, testStreamInput, (void*)&sStr);
1033     }
1034   }
1035   if( rc!=SQLITE_OK ){
1036     return test_session_error(interp, rc, 0);
1037   }
1038 
1039   while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
1040     int nCol;                     /* Number of columns in table */
1041     int nCol2;                    /* Number of columns in table */
1042     int op;                       /* SQLITE_INSERT, UPDATE or DELETE */
1043     const char *zTab;             /* Name of table change applies to */
1044     Tcl_Obj *pVar;                /* Tcl value to set $VARNAME to */
1045     Tcl_Obj *pOld;                /* Vector of old.* values */
1046     Tcl_Obj *pNew;                /* Vector of new.* values */
1047     int bIndirect;
1048 
1049     char *zPK;
1050     unsigned char *abPK;
1051     int i;
1052 
1053     /* Test that _fk_conflicts() returns SQLITE_MISUSE if called on this
1054     ** iterator. */
1055     int nDummy;
1056     if( SQLITE_MISUSE!=sqlite3changeset_fk_conflicts(pIter, &nDummy) ){
1057       sqlite3changeset_finalize(pIter);
1058       return TCL_ERROR;
1059     }
1060 
1061     sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
1062     pVar = Tcl_NewObj();
1063     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(
1064           op==SQLITE_INSERT ? "INSERT" :
1065           op==SQLITE_UPDATE ? "UPDATE" :
1066           "DELETE", -1
1067     ));
1068 
1069     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1));
1070     Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect));
1071 
1072     zPK = ckalloc(nCol+1);
1073     memset(zPK, 0, nCol+1);
1074     sqlite3changeset_pk(pIter, &abPK, &nCol2);
1075     assert( nCol==nCol2 );
1076     for(i=0; i<nCol; i++){
1077       zPK[i] = (abPK[i] ? 'X' : '.');
1078     }
1079     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zPK, -1));
1080     ckfree(zPK);
1081 
1082     pOld = Tcl_NewObj();
1083     if( op!=SQLITE_INSERT ){
1084       for(i=0; i<nCol; i++){
1085         sqlite3_value *pVal;
1086         sqlite3changeset_old(pIter, i, &pVal);
1087         test_append_value(pOld, pVal);
1088       }
1089     }
1090     pNew = Tcl_NewObj();
1091     if( op!=SQLITE_DELETE ){
1092       for(i=0; i<nCol; i++){
1093         sqlite3_value *pVal;
1094         sqlite3changeset_new(pIter, i, &pVal);
1095         test_append_value(pNew, pVal);
1096       }
1097     }
1098     Tcl_ListObjAppendElement(0, pVar, pOld);
1099     Tcl_ListObjAppendElement(0, pVar, pNew);
1100 
1101     Tcl_ObjSetVar2(interp, pVarname, 0, pVar, 0);
1102     rc = Tcl_EvalObjEx(interp, pScript, 0);
1103     if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
1104       sqlite3changeset_finalize(pIter);
1105       return rc==TCL_BREAK ? TCL_OK : rc;
1106     }
1107   }
1108 
1109   if( isCheckNext ){
1110     int rc2 = sqlite3changeset_next(pIter);
1111     rc = sqlite3changeset_finalize(pIter);
1112     assert( (rc2==SQLITE_DONE && rc==SQLITE_OK) || rc2==rc );
1113   }else{
1114     rc = sqlite3changeset_finalize(pIter);
1115   }
1116   if( rc!=SQLITE_OK ){
1117     return test_session_error(interp, rc, 0);
1118   }
1119 
1120   return TCL_OK;
1121 }
1122 
1123 /*
1124 ** tclcmd: CMD configure REBASE-BLOB
1125 ** tclcmd: CMD rebase CHANGESET
1126 ** tclcmd: CMD delete
1127 */
1128 static int SQLITE_TCLAPI test_rebaser_cmd(
1129   void * clientData,
1130   Tcl_Interp *interp,
1131   int objc,
1132   Tcl_Obj *CONST objv[]
1133 ){
1134   static struct RebaseSubcmd {
1135     const char *zSub;
1136     int nArg;
1137     const char *zMsg;
1138     int iSub;
1139   } aSub[] = {
1140     { "configure",    1, "REBASE-BLOB" }, /* 0 */
1141     { "delete",       0, ""            }, /* 1 */
1142     { "rebase",       1, "CHANGESET"   }, /* 2 */
1143     { 0 }
1144   };
1145 
1146   sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
1147   int iSub;
1148   int rc;
1149 
1150   if( objc<2 ){
1151     Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
1152     return TCL_ERROR;
1153   }
1154   rc = Tcl_GetIndexFromObjStruct(interp,
1155       objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
1156   );
1157   if( rc!=TCL_OK ) return rc;
1158   if( objc!=2+aSub[iSub].nArg ){
1159     Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
1160     return TCL_ERROR;
1161   }
1162 
1163   assert( iSub==0 || iSub==1 || iSub==2 );
1164   assert( rc==SQLITE_OK );
1165   switch( iSub ){
1166     case 0: {   /* configure */
1167       int nRebase = 0;
1168       unsigned char *pRebase = Tcl_GetByteArrayFromObj(objv[2], &nRebase);
1169       rc = sqlite3rebaser_configure(p, nRebase, pRebase);
1170       break;
1171     }
1172 
1173     case 1:     /* delete */
1174       Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
1175       break;
1176 
1177     default: {  /* rebase */
1178       TestStreamInput sStr;                 /* Input stream */
1179       TestSessionsBlob sOut;                /* Output blob */
1180 
1181       memset(&sStr, 0, sizeof(sStr));
1182       memset(&sOut, 0, sizeof(sOut));
1183       sStr.aData = Tcl_GetByteArrayFromObj(objv[2], &sStr.nData);
1184       sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
1185 
1186       if( sStr.nStream ){
1187         rc = sqlite3rebaser_rebase_strm(p,
1188             testStreamInput, (void*)&sStr,
1189             testStreamOutput, (void*)&sOut
1190         );
1191       }else{
1192         rc = sqlite3rebaser_rebase(p, sStr.nData, sStr.aData, &sOut.n, &sOut.p);
1193       }
1194 
1195       if( rc==SQLITE_OK ){
1196         Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n));
1197       }
1198       sqlite3_free(sOut.p);
1199       break;
1200     }
1201   }
1202 
1203   if( rc!=SQLITE_OK ){
1204     return test_session_error(interp, rc, 0);
1205   }
1206   return TCL_OK;
1207 }
1208 
1209 static void SQLITE_TCLAPI test_rebaser_del(void *clientData){
1210   sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
1211   sqlite3rebaser_delete(p);
1212 }
1213 
1214 /*
1215 ** tclcmd: sqlite3rebaser_create NAME
1216 */
1217 static int SQLITE_TCLAPI test_sqlite3rebaser_create(
1218   void * clientData,
1219   Tcl_Interp *interp,
1220   int objc,
1221   Tcl_Obj *CONST objv[]
1222 ){
1223   int rc;
1224   sqlite3_rebaser *pNew = 0;
1225   if( objc!=2 ){
1226     Tcl_WrongNumArgs(interp, 1, objv, "NAME");
1227     return SQLITE_ERROR;
1228   }
1229 
1230   rc = sqlite3rebaser_create(&pNew);
1231   if( rc!=SQLITE_OK ){
1232     return test_session_error(interp, rc, 0);
1233   }
1234 
1235   Tcl_CreateObjCommand(interp, Tcl_GetString(objv[1]), test_rebaser_cmd,
1236       (ClientData)pNew, test_rebaser_del
1237   );
1238   Tcl_SetObjResult(interp, objv[1]);
1239   return TCL_OK;
1240 }
1241 
1242 /*
1243 ** tclcmd: sqlite3rebaser_configure OP VALUE
1244 */
1245 static int SQLITE_TCLAPI test_sqlite3session_config(
1246   void * clientData,
1247   Tcl_Interp *interp,
1248   int objc,
1249   Tcl_Obj *CONST objv[]
1250 ){
1251   static struct ConfigOpt {
1252     const char *zSub;
1253     int op;
1254   } aSub[] = {
1255     { "strm_size",    SQLITE_SESSION_CONFIG_STRMSIZE },
1256     { "invalid",      0 },
1257     { 0 }
1258   };
1259   int rc;
1260   int iSub;
1261   int iVal;
1262 
1263   if( objc!=3 ){
1264     Tcl_WrongNumArgs(interp, 1, objv, "OP VALUE");
1265     return SQLITE_ERROR;
1266   }
1267   rc = Tcl_GetIndexFromObjStruct(interp,
1268       objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
1269   );
1270   if( rc!=TCL_OK ) return rc;
1271   if( Tcl_GetIntFromObj(interp, objv[2], &iVal) ) return TCL_ERROR;
1272 
1273   rc = sqlite3session_config(aSub[iSub].op, (void*)&iVal);
1274   if( rc!=SQLITE_OK ){
1275     return test_session_error(interp, rc, 0);
1276   }
1277   Tcl_SetObjResult(interp, Tcl_NewIntObj(iVal));
1278   return TCL_OK;
1279 }
1280 
1281 int TestSession_Init(Tcl_Interp *interp){
1282   struct Cmd {
1283     const char *zCmd;
1284     Tcl_ObjCmdProc *xProc;
1285   } aCmd[] = {
1286     { "sqlite3session", test_sqlite3session },
1287     { "sqlite3session_foreach", test_sqlite3session_foreach },
1288     { "sqlite3changeset_invert", test_sqlite3changeset_invert },
1289     { "sqlite3changeset_concat", test_sqlite3changeset_concat },
1290     { "sqlite3changeset_apply", test_sqlite3changeset_apply },
1291     { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 },
1292     { "sqlite3changeset_apply_replace_all",
1293       test_sqlite3changeset_apply_replace_all },
1294     { "sql_exec_changeset", test_sql_exec_changeset },
1295     { "sqlite3rebaser_create", test_sqlite3rebaser_create },
1296     { "sqlite3session_config", test_sqlite3session_config },
1297   };
1298   int i;
1299 
1300   for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
1301     struct Cmd *p = &aCmd[i];
1302     Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0);
1303   }
1304 
1305   return TCL_OK;
1306 }
1307 
1308 #endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */
1309