xref: /sqlite-3.40.0/ext/session/test_session.c (revision fb32c44e)
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   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 && objc>1 ){
741     const char *z1 = Tcl_GetString(objv[1]);
742     int n = strlen(z1);
743     if( n>1 && n<=12 && 0==sqlite3_strnicmp("-nosavepoint", z1, n) ){
744       flags = SQLITE_CHANGESETAPPLY_NOSAVEPOINT;
745       objc--;
746       objv++;
747     }
748   }
749 
750   if( objc!=4 && objc!=5 ){
751     const char *zMsg;
752     if( bV2 ){
753       zMsg = "?-nosavepoint? DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
754     }else{
755       zMsg = "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
756     }
757     Tcl_WrongNumArgs(interp, 1, objv, zMsg);
758     return TCL_ERROR;
759   }
760   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
761     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[1]), 0);
762     return TCL_ERROR;
763   }
764   db = *(sqlite3 **)info.objClientData;
765   pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
766   ctx.pConflictScript = objv[3];
767   ctx.pFilterScript = objc==5 ? objv[4] : 0;
768   ctx.interp = interp;
769 
770   if( sStr.nStream==0 ){
771     if( bV2==0 ){
772       rc = sqlite3changeset_apply(db, nChangeset, pChangeset,
773           (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx
774       );
775     }else{
776       rc = sqlite3changeset_apply_v2(db, nChangeset, pChangeset,
777           (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx,
778           &pRebase, &nRebase, flags
779       );
780     }
781   }else{
782     sStr.aData = (unsigned char*)pChangeset;
783     sStr.nData = nChangeset;
784     if( bV2==0 ){
785       rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr,
786           (objc==5) ? test_filter_handler : 0,
787           test_conflict_handler, (void *)&ctx
788       );
789     }else{
790       rc = sqlite3changeset_apply_v2_strm(db, testStreamInput, (void*)&sStr,
791           (objc==5) ? test_filter_handler : 0,
792           test_conflict_handler, (void *)&ctx,
793           &pRebase, &nRebase, flags
794       );
795     }
796   }
797 
798   if( rc!=SQLITE_OK ){
799     return test_session_error(interp, rc, 0);
800   }else{
801     Tcl_ResetResult(interp);
802     if( bV2 && pRebase ){
803       Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pRebase, nRebase));
804     }
805   }
806   sqlite3_free(pRebase);
807   return TCL_OK;
808 }
809 
810 /*
811 ** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
812 */
813 static int SQLITE_TCLAPI test_sqlite3changeset_apply(
814   void * clientData,
815   Tcl_Interp *interp,
816   int objc,
817   Tcl_Obj *CONST objv[]
818 ){
819   return testSqlite3changesetApply(0, clientData, interp, objc, objv);
820 }
821 /*
822 ** sqlite3changeset_apply_v2 DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
823 */
824 static int SQLITE_TCLAPI test_sqlite3changeset_apply_v2(
825   void * clientData,
826   Tcl_Interp *interp,
827   int objc,
828   Tcl_Obj *CONST objv[]
829 ){
830   return testSqlite3changesetApply(1, clientData, interp, objc, objv);
831 }
832 
833 /*
834 ** sqlite3changeset_apply_replace_all DB CHANGESET
835 */
836 static int SQLITE_TCLAPI test_sqlite3changeset_apply_replace_all(
837   void * clientData,
838   Tcl_Interp *interp,
839   int objc,
840   Tcl_Obj *CONST objv[]
841 ){
842   sqlite3 *db;                    /* Database handle */
843   Tcl_CmdInfo info;               /* Database Tcl command (objv[1]) info */
844   int rc;                         /* Return code from changeset_invert() */
845   void *pChangeset;               /* Buffer containing changeset */
846   int nChangeset;                 /* Size of buffer aChangeset in bytes */
847 
848   if( objc!=3 ){
849     Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET");
850     return TCL_ERROR;
851   }
852   if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
853     Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
854     return TCL_ERROR;
855   }
856   db = *(sqlite3 **)info.objClientData;
857   pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
858 
859   rc = sqlite3changeset_apply(db, nChangeset, pChangeset, 0, replace_handler,0);
860   if( rc!=SQLITE_OK ){
861     return test_session_error(interp, rc, 0);
862   }
863   Tcl_ResetResult(interp);
864   return TCL_OK;
865 }
866 
867 
868 /*
869 ** sqlite3changeset_invert CHANGESET
870 */
871 static int SQLITE_TCLAPI test_sqlite3changeset_invert(
872   void * clientData,
873   Tcl_Interp *interp,
874   int objc,
875   Tcl_Obj *CONST objv[]
876 ){
877   int rc;                         /* Return code from changeset_invert() */
878   TestStreamInput sIn;            /* Input stream */
879   TestSessionsBlob sOut;          /* Output blob */
880 
881   if( objc!=2 ){
882     Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
883     return TCL_ERROR;
884   }
885 
886   memset(&sIn, 0, sizeof(sIn));
887   memset(&sOut, 0, sizeof(sOut));
888   sIn.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
889   sIn.aData = Tcl_GetByteArrayFromObj(objv[1], &sIn.nData);
890 
891   if( sIn.nStream ){
892     rc = sqlite3changeset_invert_strm(
893         testStreamInput, (void*)&sIn, testStreamOutput, (void*)&sOut
894     );
895   }else{
896     rc = sqlite3changeset_invert(sIn.nData, sIn.aData, &sOut.n, &sOut.p);
897   }
898   if( rc!=SQLITE_OK ){
899     rc = test_session_error(interp, rc, 0);
900   }else{
901     Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
902   }
903   sqlite3_free(sOut.p);
904   return rc;
905 }
906 
907 /*
908 ** sqlite3changeset_concat LEFT RIGHT
909 */
910 static int SQLITE_TCLAPI test_sqlite3changeset_concat(
911   void * clientData,
912   Tcl_Interp *interp,
913   int objc,
914   Tcl_Obj *CONST objv[]
915 ){
916   int rc;                         /* Return code from changeset_invert() */
917 
918   TestStreamInput sLeft;          /* Input stream */
919   TestStreamInput sRight;         /* Input stream */
920   TestSessionsBlob sOut = {0,0};  /* Output blob */
921 
922   if( objc!=3 ){
923     Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT");
924     return TCL_ERROR;
925   }
926 
927   memset(&sLeft, 0, sizeof(sLeft));
928   memset(&sRight, 0, sizeof(sRight));
929   sLeft.aData = Tcl_GetByteArrayFromObj(objv[1], &sLeft.nData);
930   sRight.aData = Tcl_GetByteArrayFromObj(objv[2], &sRight.nData);
931   sLeft.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
932   sRight.nStream = sLeft.nStream;
933 
934   if( sLeft.nStream>0 ){
935     rc = sqlite3changeset_concat_strm(
936         testStreamInput, (void*)&sLeft,
937         testStreamInput, (void*)&sRight,
938         testStreamOutput, (void*)&sOut
939     );
940   }else{
941     rc = sqlite3changeset_concat(
942         sLeft.nData, sLeft.aData, sRight.nData, sRight.aData, &sOut.n, &sOut.p
943     );
944   }
945 
946   if( rc!=SQLITE_OK ){
947     rc = test_session_error(interp, rc, 0);
948   }else{
949     Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
950   }
951   sqlite3_free(sOut.p);
952   return rc;
953 }
954 
955 /*
956 ** sqlite3session_foreach VARNAME CHANGESET SCRIPT
957 */
958 static int SQLITE_TCLAPI test_sqlite3session_foreach(
959   void * clientData,
960   Tcl_Interp *interp,
961   int objc,
962   Tcl_Obj *CONST objv[]
963 ){
964   void *pChangeset;
965   int nChangeset;
966   sqlite3_changeset_iter *pIter;
967   int rc;
968   Tcl_Obj *pVarname;
969   Tcl_Obj *pCS;
970   Tcl_Obj *pScript;
971   int isCheckNext = 0;
972 
973   TestStreamInput sStr;
974   memset(&sStr, 0, sizeof(sStr));
975 
976   if( objc>1 ){
977     char *zOpt = Tcl_GetString(objv[1]);
978     isCheckNext = (strcmp(zOpt, "-next")==0);
979   }
980   if( objc!=4+isCheckNext ){
981     Tcl_WrongNumArgs(interp, 1, objv, "?-next? VARNAME CHANGESET SCRIPT");
982     return TCL_ERROR;
983   }
984 
985   pVarname = objv[1+isCheckNext];
986   pCS = objv[2+isCheckNext];
987   pScript = objv[3+isCheckNext];
988 
989   pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset);
990   sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
991   if( sStr.nStream==0 ){
992     rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
993   }else{
994     sStr.aData = (unsigned char*)pChangeset;
995     sStr.nData = nChangeset;
996     rc = sqlite3changeset_start_strm(&pIter, testStreamInput, (void*)&sStr);
997   }
998   if( rc!=SQLITE_OK ){
999     return test_session_error(interp, rc, 0);
1000   }
1001 
1002   while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
1003     int nCol;                     /* Number of columns in table */
1004     int nCol2;                    /* Number of columns in table */
1005     int op;                       /* SQLITE_INSERT, UPDATE or DELETE */
1006     const char *zTab;             /* Name of table change applies to */
1007     Tcl_Obj *pVar;                /* Tcl value to set $VARNAME to */
1008     Tcl_Obj *pOld;                /* Vector of old.* values */
1009     Tcl_Obj *pNew;                /* Vector of new.* values */
1010     int bIndirect;
1011 
1012     char *zPK;
1013     unsigned char *abPK;
1014     int i;
1015 
1016     /* Test that _fk_conflicts() returns SQLITE_MISUSE if called on this
1017     ** iterator. */
1018     int nDummy;
1019     if( SQLITE_MISUSE!=sqlite3changeset_fk_conflicts(pIter, &nDummy) ){
1020       sqlite3changeset_finalize(pIter);
1021       return TCL_ERROR;
1022     }
1023 
1024     sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
1025     pVar = Tcl_NewObj();
1026     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(
1027           op==SQLITE_INSERT ? "INSERT" :
1028           op==SQLITE_UPDATE ? "UPDATE" :
1029           "DELETE", -1
1030     ));
1031 
1032     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1));
1033     Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect));
1034 
1035     zPK = ckalloc(nCol+1);
1036     memset(zPK, 0, nCol+1);
1037     sqlite3changeset_pk(pIter, &abPK, &nCol2);
1038     assert( nCol==nCol2 );
1039     for(i=0; i<nCol; i++){
1040       zPK[i] = (abPK[i] ? 'X' : '.');
1041     }
1042     Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zPK, -1));
1043     ckfree(zPK);
1044 
1045     pOld = Tcl_NewObj();
1046     if( op!=SQLITE_INSERT ){
1047       for(i=0; i<nCol; i++){
1048         sqlite3_value *pVal;
1049         sqlite3changeset_old(pIter, i, &pVal);
1050         test_append_value(pOld, pVal);
1051       }
1052     }
1053     pNew = Tcl_NewObj();
1054     if( op!=SQLITE_DELETE ){
1055       for(i=0; i<nCol; i++){
1056         sqlite3_value *pVal;
1057         sqlite3changeset_new(pIter, i, &pVal);
1058         test_append_value(pNew, pVal);
1059       }
1060     }
1061     Tcl_ListObjAppendElement(0, pVar, pOld);
1062     Tcl_ListObjAppendElement(0, pVar, pNew);
1063 
1064     Tcl_ObjSetVar2(interp, pVarname, 0, pVar, 0);
1065     rc = Tcl_EvalObjEx(interp, pScript, 0);
1066     if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
1067       sqlite3changeset_finalize(pIter);
1068       return rc==TCL_BREAK ? TCL_OK : rc;
1069     }
1070   }
1071 
1072   if( isCheckNext ){
1073     int rc2 = sqlite3changeset_next(pIter);
1074     rc = sqlite3changeset_finalize(pIter);
1075     assert( (rc2==SQLITE_DONE && rc==SQLITE_OK) || rc2==rc );
1076   }else{
1077     rc = sqlite3changeset_finalize(pIter);
1078   }
1079   if( rc!=SQLITE_OK ){
1080     return test_session_error(interp, rc, 0);
1081   }
1082 
1083   return TCL_OK;
1084 }
1085 
1086 /*
1087 ** tclcmd: CMD configure REBASE-BLOB
1088 ** tclcmd: CMD rebase CHANGESET
1089 ** tclcmd: CMD delete
1090 */
1091 static int SQLITE_TCLAPI test_rebaser_cmd(
1092   void * clientData,
1093   Tcl_Interp *interp,
1094   int objc,
1095   Tcl_Obj *CONST objv[]
1096 ){
1097   struct RebaseSubcmd {
1098     const char *zSub;
1099     int nArg;
1100     const char *zMsg;
1101     int iSub;
1102   } aSub[] = {
1103     { "configure",    1, "REBASE-BLOB" }, /* 0 */
1104     { "delete",       0, ""            }, /* 1 */
1105     { "rebase",       1, "CHANGESET"   }, /* 2 */
1106     { 0 }
1107   };
1108 
1109   sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
1110   int iSub;
1111   int rc;
1112 
1113   if( objc<2 ){
1114     Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
1115     return TCL_ERROR;
1116   }
1117   rc = Tcl_GetIndexFromObjStruct(interp,
1118       objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
1119   );
1120   if( rc!=TCL_OK ) return rc;
1121   if( objc!=2+aSub[iSub].nArg ){
1122     Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
1123     return TCL_ERROR;
1124   }
1125 
1126   assert( iSub==0 || iSub==1 || iSub==2 );
1127   assert( rc==SQLITE_OK );
1128   switch( iSub ){
1129     case 0: {   /* configure */
1130       int nRebase = 0;
1131       unsigned char *pRebase = Tcl_GetByteArrayFromObj(objv[2], &nRebase);
1132       rc = sqlite3rebaser_configure(p, nRebase, pRebase);
1133       break;
1134     }
1135 
1136     case 1:     /* delete */
1137       Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
1138       break;
1139 
1140     default: {  /* rebase */
1141       TestStreamInput sStr;                 /* Input stream */
1142       TestSessionsBlob sOut;                /* Output blob */
1143 
1144       memset(&sStr, 0, sizeof(sStr));
1145       memset(&sOut, 0, sizeof(sOut));
1146       sStr.aData = Tcl_GetByteArrayFromObj(objv[2], &sStr.nData);
1147       sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
1148 
1149       if( sStr.nStream ){
1150         rc = sqlite3rebaser_rebase_strm(p,
1151             testStreamInput, (void*)&sStr,
1152             testStreamOutput, (void*)&sOut
1153         );
1154       }else{
1155         rc = sqlite3rebaser_rebase(p, sStr.nData, sStr.aData, &sOut.n, &sOut.p);
1156       }
1157 
1158       if( rc==SQLITE_OK ){
1159         Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n));
1160       }
1161       sqlite3_free(sOut.p);
1162       break;
1163     }
1164   }
1165 
1166   if( rc!=SQLITE_OK ){
1167     return test_session_error(interp, rc, 0);
1168   }
1169   return TCL_OK;
1170 }
1171 
1172 static void SQLITE_TCLAPI test_rebaser_del(void *clientData){
1173   sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
1174   sqlite3rebaser_delete(p);
1175 }
1176 
1177 /*
1178 ** tclcmd: sqlite3rebaser_create NAME
1179 */
1180 static int SQLITE_TCLAPI test_sqlite3rebaser_create(
1181   void * clientData,
1182   Tcl_Interp *interp,
1183   int objc,
1184   Tcl_Obj *CONST objv[]
1185 ){
1186   int rc;
1187   sqlite3_rebaser *pNew = 0;
1188   if( objc!=2 ){
1189     Tcl_WrongNumArgs(interp, 1, objv, "NAME");
1190     return SQLITE_ERROR;
1191   }
1192 
1193   rc = sqlite3rebaser_create(&pNew);
1194   if( rc!=SQLITE_OK ){
1195     return test_session_error(interp, rc, 0);
1196   }
1197 
1198   Tcl_CreateObjCommand(interp, Tcl_GetString(objv[1]), test_rebaser_cmd,
1199       (ClientData)pNew, test_rebaser_del
1200   );
1201   Tcl_SetObjResult(interp, objv[1]);
1202   return TCL_OK;
1203 }
1204 
1205 int TestSession_Init(Tcl_Interp *interp){
1206   struct Cmd {
1207     const char *zCmd;
1208     Tcl_ObjCmdProc *xProc;
1209   } aCmd[] = {
1210     { "sqlite3session", test_sqlite3session },
1211     { "sqlite3session_foreach", test_sqlite3session_foreach },
1212     { "sqlite3changeset_invert", test_sqlite3changeset_invert },
1213     { "sqlite3changeset_concat", test_sqlite3changeset_concat },
1214     { "sqlite3changeset_apply", test_sqlite3changeset_apply },
1215     { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 },
1216     { "sqlite3changeset_apply_replace_all",
1217       test_sqlite3changeset_apply_replace_all },
1218     { "sql_exec_changeset", test_sql_exec_changeset },
1219     { "sqlite3rebaser_create", test_sqlite3rebaser_create },
1220   };
1221   int i;
1222 
1223   for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
1224     struct Cmd *p = &aCmd[i];
1225     Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0);
1226   }
1227 
1228   return TCL_OK;
1229 }
1230 
1231 #endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */
1232