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