SQLite

Check-in [cf0d1abb44]
Login

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

Overview
Comment:Add simple tests for the sessions module rebase API.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | sessions-rebase
Files: files | file ages | folders
SHA3-256: cf0d1abb44cf170d747e9c11f49ec03a29f00ab4821c613ca1e05b883a568211
User & Date: dan 2018-03-15 19:25:40.859
Context
2018-03-16
18:02
Fix a problem with handling rebasing UPDATE changes for REPLACE conflict resolution. (check-in: f7bf71f1d4 user: dan tags: sessions-rebase)
2018-03-15
19:25
Add simple tests for the sessions module rebase API. (check-in: cf0d1abb44 user: dan tags: sessions-rebase)
2018-03-14
21:06
Add largely untested APIs for rebasing changesets. (check-in: 39915b683b user: dan tags: sessions-rebase)
Changes
Unified Diff Ignore Whitespace Patch
Changes to ext/session/sessionrebase.test.
117
118
119
120
121
122
123


124



























































































































125

  UPDATE t1 SET b='value D' WHERE a=1;
} {
  REPLACE
} {
  {DELETE t1 0 X. {i 1 {} {}} {}}
}































































































































finish_test








>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
  UPDATE t1 SET b='value D' WHERE a=1;
} {
  REPLACE
} {
  {DELETE t1 0 X. {i 1 {} {}} {}}
}

#-------------------------------------------------------------------------
# Test cases 2.* - simple tests of rebasing actual changesets.
#
#    2.1.1 - 1u2u1r
#    2.1.2 - 1u2u2r
#    2.1.3 - 1d2d
#    2.1.4 - 1d2u1r
#    2.1.5 - 1d2u2r !!
#    2.1.6 - 1u2d1r

proc xConflictAbort {args} {
  return "ABORT"
}

# Take a copy of database test.db in file test.db2. Execute $sql1
# against test.db and $sql2 against test.db2. Capture a changeset
# for each. Then send the test.db2 changeset to test.db and apply
# it with the conflict handlers in $conflict_handler. Patch the
# test.db changeset and then execute it against test.db2. Test that
# the two databases come out the same.
#
proc do_rebase_test {tn sql1 sql2 conflict_handler {testsql ""} {testres ""}} {

  forcedelete test.db2 test.db2-journal test.db2-wal
  forcecopy test.db test.db2
  sqlite3 db2 test.db2

  db eval BEGIN

  sqlite3session S1 db main
  S1 attach *
  execsql $sql1 db
  set c1 [S1 changeset]
  S1 delete

  sqlite3session S2 db2 main
  S2 attach *
  execsql $sql2 db2
  set c2 [S2 changeset]
  S2 delete

  set ::lConflict $conflict_handler
  set rebase [sqlite3changeset_apply_v2 db $c2 xConflict]
  #puts [changeset_to_list $rebase]

  sqlite3rebaser_create R
  R configure $rebase
  set c1r [R rebase $c1]
  R delete
  #puts [changeset_to_list $c1r]

  sqlite3changeset_apply_v2 db2 $c1r xConflictAbort

  uplevel [list do_test $tn.1 [list compare_db db db2] {}]
  db2 close

  if {$testsql!=""} {
    uplevel [list do_execsql_test $tn.2 $testsql $testres]
  }

  db eval ROLLBACK
}

reset_db
do_execsql_test 2.1.0 {
  CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);
  INSERT INTO t1 VALUES(1, 'one');
  INSERT INTO t1 VALUES(2, 'two');
  INSERT INTO t1 VALUES(3, 'three');
}
do_rebase_test 2.1.1 {
  UPDATE t1 SET b = 'two.1' WHERE a=2;
} {
  UPDATE t1 SET b = 'two.2' WHERE a=2;
} {
  OMIT
} { SELECT * FROM t1 } {1 one 2 two.1 3 three}

do_rebase_test 2.1.2 {
  UPDATE t1 SET b = 'two.1' WHERE a=2;
} {
  UPDATE t1 SET b = 'two.2' WHERE a=2;
} {
  REPLACE
} { SELECT * FROM t1 } {1 one 2 two.2 3 three}

do_rebase_test 2.1.3 {
  DELETE FROM t1 WHERE a=3;
} {
  DELETE FROM t1 WHERE a=3;
} {
  OMIT
} { SELECT * FROM t1 } {1 one 2 two}

do_rebase_test 2.1.4 {
  DELETE FROM t1 WHERE a=1;
} {
  UPDATE t1 SET b='one.2' WHERE a=1
} {
  OMIT
} { SELECT * FROM t1 } {2 two 3 three}

#do_rebase_test 2.1.5 {
  #DELETE FROM t1 WHERE a=1;
#} {
  #UPDATE t1 SET b='one.2' WHERE a=1
#} {
  #REPLACE
#} { SELECT * FROM t1 } {2 two 3 three}

do_rebase_test 2.1.6 {
  UPDATE t1 SET b='three.1' WHERE a=3;
} {
  DELETE FROM t1 WHERE a=3;
} {
  OMIT
} { SELECT * FROM t1 } {1 one 2 two 3 three.1}

do_rebase_test 2.1.7 {
  UPDATE t1 SET b='three.1' WHERE a=3;
} {
  DELETE FROM t1 WHERE a=3;
} {
  REPLACE
} { SELECT * FROM t1 } {1 one 2 two}

finish_test

Changes to ext/session/sqlite3session.c.
4274
4275
4276
4277
4278
4279
4280
4281
4282








4283
4284

4285
4286
4287
4288
4289
4290
4291
      if( rc!=SQLITE_OK ) break;

      sqlite3_free((char*)sApply.azCol);  /* cast works around VC++ bug */
      sqlite3_finalize(sApply.pDelete);
      sqlite3_finalize(sApply.pUpdate); 
      sqlite3_finalize(sApply.pInsert);
      sqlite3_finalize(sApply.pSelect);
      memset(&sApply, 0, sizeof(sApply));
      sApply.db = db;








      sApply.bDeferConstraints = 1;
      sApply.bRebaseStarted = 0;


      /* If an xFilter() callback was specified, invoke it now. If the 
      ** xFilter callback returns zero, skip this table. If it returns
      ** non-zero, proceed. */
      schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew)));
      if( schemaMismatch ){
        zTab = sqlite3_mprintf("%s", zNew);







<

>
>
>
>
>
>
>
>


>







4274
4275
4276
4277
4278
4279
4280

4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
      if( rc!=SQLITE_OK ) break;

      sqlite3_free((char*)sApply.azCol);  /* cast works around VC++ bug */
      sqlite3_finalize(sApply.pDelete);
      sqlite3_finalize(sApply.pUpdate); 
      sqlite3_finalize(sApply.pInsert);
      sqlite3_finalize(sApply.pSelect);

      sApply.db = db;
      sApply.pDelete = 0;
      sApply.pUpdate = 0;
      sApply.pInsert = 0;
      sApply.pSelect = 0;
      sApply.nCol = 0;
      sApply.azCol = 0;
      sApply.abPK = 0;
      sApply.bStat1 = 0;
      sApply.bDeferConstraints = 1;
      sApply.bRebaseStarted = 0;
      memset(&sApply.constraints, 0, sizeof(SessionBuffer));

      /* If an xFilter() callback was specified, invoke it now. If the 
      ** xFilter callback returns zero, skip this table. If it returns
      ** non-zero, proceed. */
      schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew)));
      if( schemaMismatch ){
        zTab = sqlite3_mprintf("%s", zNew);
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
  int bRebase                      /* True if hash table is for rebasing */
){
  u8 *aRec;
  int nRec;
  int rc = SQLITE_OK;
  SessionTable *pTab = 0;


  while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, 0) ){
    const char *zNew;
    int nCol;
    int op;
    int iHash;
    int bIndirect;
    SessionChange *pChange;







<







4662
4663
4664
4665
4666
4667
4668

4669
4670
4671
4672
4673
4674
4675
  int bRebase                      /* True if hash table is for rebasing */
){
  u8 *aRec;
  int nRec;
  int rc = SQLITE_OK;
  SessionTable *pTab = 0;


  while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, 0) ){
    const char *zNew;
    int nCol;
    int op;
    int iHash;
    int bIndirect;
    SessionChange *pChange;
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015



5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
          memcpy(pOut, a2, nn2);
          pOut += nn2;
        }else{
          memcpy(pOut, a1, nn1);
          pOut += nn1;
        }
      }
      a1 += n1;
      a2 += n2;
    }



  }
}

static int sessionRebase(
  sqlite3_rebaser *p,             /* Rebaser hash table */
  sqlite3_changeset_iter *pIter,  /* Input data */
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut,                     /* Context for xOutput callback */
  int *pnOut,                     /* OUT: Number of bytes in output changeset */
  void **ppOut                    /* OUT: Inverse of pChangeset */
){
  int rc = SQLITE_OK;
  u8 *aRec = 0;
  int nRec = 0;
  int bNew = 0;
  SessionTable *pTab = 0;
  SessionBuffer sOut = {0,0,0};

  while( SQLITE_OK==sessionChangesetNext(pIter, &aRec, &nRec, &bNew) ){
    SessionChange *pChange = 0;

    if( bNew ){
      const char *zTab = pIter->zTab;
      for(pTab=p->grp.pList; pTab; pTab=pTab->pNext){
        if( 0==sqlite3_stricmp(pTab->zName, zTab) ) break;
      }







|
|

>
>
>


















|







5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
          memcpy(pOut, a2, nn2);
          pOut += nn2;
        }else{
          memcpy(pOut, a1, nn1);
          pOut += nn1;
        }
      }
      a1 += nn1;
      a2 += nn2;
    }

    pBuf->nBuf = pOut-pBuf->aBuf;
    assert( pBuf->nBuf<=pBuf->nAlloc );
  }
}

static int sessionRebase(
  sqlite3_rebaser *p,             /* Rebaser hash table */
  sqlite3_changeset_iter *pIter,  /* Input data */
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut,                     /* Context for xOutput callback */
  int *pnOut,                     /* OUT: Number of bytes in output changeset */
  void **ppOut                    /* OUT: Inverse of pChangeset */
){
  int rc = SQLITE_OK;
  u8 *aRec = 0;
  int nRec = 0;
  int bNew = 0;
  SessionTable *pTab = 0;
  SessionBuffer sOut = {0,0,0};

  while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, &bNew) ){
    SessionChange *pChange = 0;

    if( bNew ){
      const char *zTab = pIter->zTab;
      for(pTab=p->grp.pList; pTab; pTab=pTab->pNext){
        if( 0==sqlite3_stricmp(pTab->zName, zTab) ) break;
      }
5074
5075
5076
5077
5078
5079
5080


5081

5082
5083
5084
5085
5086
5087
5088
5089
          */
          if( pIter->op!=SQLITE_DELETE ){
            sessionAppendByte(&sOut, SQLITE_INSERT, &rc);
            sessionAppendByte(&sOut, pIter->bIndirect, &rc);
            if( pIter->op==SQLITE_INSERT ){
              sessionAppendBlob(&sOut, aRec, nRec, &rc);
            }else{


              sessionAppendRecordMerge(&sOut, pIter->nCol, 1,

                  aRec, nRec, pChange->aRecord, pChange->nRecord, &rc
              );
            }
          }
        }else{
          sessionAppendByte(&sOut, pIter->op, &rc);
          sessionAppendByte(&sOut, pIter->bIndirect, &rc);
          if( pIter->op==SQLITE_INSERT ){







>
>

>
|







5084
5085
5086
5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
5100
5101
5102
          */
          if( pIter->op!=SQLITE_DELETE ){
            sessionAppendByte(&sOut, SQLITE_INSERT, &rc);
            sessionAppendByte(&sOut, pIter->bIndirect, &rc);
            if( pIter->op==SQLITE_INSERT ){
              sessionAppendBlob(&sOut, aRec, nRec, &rc);
            }else{
              u8 *pCsr = aRec;
              sessionSkipRecord(&pCsr, pIter->nCol);
              sessionAppendRecordMerge(&sOut, pIter->nCol, 1,
                  pCsr, nRec-(pCsr-aRec), 
                  pChange->aRecord, pChange->nRecord, &rc
              );
            }
          }
        }else{
          sessionAppendByte(&sOut, pIter->op, &rc);
          sessionAppendByte(&sOut, pIter->bIndirect, &rc);
          if( pIter->op==SQLITE_INSERT ){
5139
5140
5141
5142
5143
5144
5145


5146
5147
5148
5149
5150
5151
5152
int sqlite3rebaser_create(sqlite3_rebaser **ppNew){
  int rc = SQLITE_OK;
  sqlite3_rebaser *pNew;

  pNew = sqlite3_malloc(sizeof(sqlite3_rebaser));
  if( pNew==0 ){
    rc = SQLITE_NOMEM;


  }
  *ppNew = pNew;
  return rc;
}

/* 
** Call this one or more times to configure a rebaser.







>
>







5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
int sqlite3rebaser_create(sqlite3_rebaser **ppNew){
  int rc = SQLITE_OK;
  sqlite3_rebaser *pNew;

  pNew = sqlite3_malloc(sizeof(sqlite3_rebaser));
  if( pNew==0 ){
    rc = SQLITE_NOMEM;
  }else{
    memset(pNew, 0, sizeof(sqlite3_rebaser));
  }
  *ppNew = pNew;
  return rc;
}

/* 
** Call this one or more times to configure a rebaser.
5204
5205
5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218

  return rc;
}

/* 
** Destroy a rebaser object 
*/
void sqlite3rebaser_destroy(sqlite3_rebaser *p){
  if( p ){
    sessionDeleteTable(p->grp.pList);
    sqlite3_free(p);
  }
}

#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */







|







5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233

  return rc;
}

/* 
** Destroy a rebaser object 
*/
void sqlite3rebaser_delete(sqlite3_rebaser *p){
  if( p ){
    sessionDeleteTable(p->grp.pList);
    sqlite3_free(p);
  }
}

#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
Changes to ext/session/sqlite3session.h.
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
int sqlite3rebaser_rebase(
  sqlite3_rebaser*,
  int nIn, const void *pIn, 
  int *pnOut, void **ppOut 
);

/* Destroy a rebaser object */
void sqlite3rebaser_destroy(sqlite3_rebaser *p); 

/*
** CAPI3REF: Streaming Versions of API functions.
**
** The six streaming API xxx_strm() functions serve similar purposes to the 
** corresponding non-streaming API functions:
**







|







1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
int sqlite3rebaser_rebase(
  sqlite3_rebaser*,
  int nIn, const void *pIn, 
  int *pnOut, void **ppOut 
);

/* Destroy a rebaser object */
void sqlite3rebaser_delete(sqlite3_rebaser *p); 

/*
** CAPI3REF: Streaming Versions of API functions.
**
** The six streaming API xxx_strm() functions serve similar purposes to the 
** corresponding non-streaming API functions:
**
Changes to ext/session/test_session.c.
9
10
11
12
13
14
15




16
17
18
19
20
21
22
#  include "sqlite_tcl.h"
#else
#  include "tcl.h"
#  ifndef SQLITE_TCLAPI
#    define SQLITE_TCLAPI
#  endif
#endif





typedef struct TestSession TestSession;
struct TestSession {
  sqlite3_session *pSession;
  Tcl_Interp *interp;
  Tcl_Obj *pFilterScript;
};







>
>
>
>







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  include "sqlite_tcl.h"
#else
#  include "tcl.h"
#  ifndef SQLITE_TCLAPI
#    define SQLITE_TCLAPI
#  endif
#endif

#ifndef SQLITE_AMALGAMATION
  typedef unsigned char u8;
#endif

typedef struct TestSession TestSession;
struct TestSession {
  sqlite3_session *pSession;
  Tcl_Interp *interp;
  Tcl_Obj *pFilterScript;
};
1058
1059
1060
1061
1062
1063
1064























































































































1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079

1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
  }
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  return TCL_OK;
}
























































































































int TestSession_Init(Tcl_Interp *interp){
  struct Cmd {
    const char *zCmd;
    Tcl_ObjCmdProc *xProc;
  } aCmd[] = {
    { "sqlite3session", test_sqlite3session },
    { "sqlite3session_foreach", test_sqlite3session_foreach },
    { "sqlite3changeset_invert", test_sqlite3changeset_invert },
    { "sqlite3changeset_concat", test_sqlite3changeset_concat },
    { "sqlite3changeset_apply", test_sqlite3changeset_apply },
    { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 },
    { "sqlite3changeset_apply_replace_all", 
      test_sqlite3changeset_apply_replace_all },
    { "sql_exec_changeset", test_sql_exec_changeset },

  };
  int i;

  for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
    struct Cmd *p = &aCmd[i];
    Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0);
  }

  return TCL_OK;
}

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







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















>












1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
  }
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  return TCL_OK;
}

/*
** tclcmd: CMD configure REBASE-BLOB
** tclcmd: CMD rebase CHANGESET
** tclcmd: CMD delete
*/
static int SQLITE_TCLAPI test_rebaser_cmd(
  void * clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
  struct RebaseSubcmd {
    const char *zSub;
    int nArg;
    const char *zMsg;
    int iSub;
  } aSub[] = {
    { "configure",    1, "REBASE-BLOB" }, /* 0 */
    { "delete",       0, ""            }, /* 1 */
    { "rebase",       1, "CHANGESET"   }, /* 2 */
    { 0 }
  };

  sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
  int iSub;
  int rc;

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

  assert( iSub==0 || iSub==1 || iSub==2 );
  assert( rc==SQLITE_OK );
  switch( iSub ){
    case 0: {   /* configure */
      int nRebase = 0;
      unsigned char *pRebase = Tcl_GetByteArrayFromObj(objv[2], &nRebase);
      rc = sqlite3rebaser_configure(p, nRebase, pRebase);
      break;
    }

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

    default: {  /* rebase */
      TestStreamInput sStr;                 /* Input stream */
      TestSessionsBlob sOut;                /* Output blob */

      memset(&sStr, 0, sizeof(sStr));
      memset(&sOut, 0, sizeof(sOut));
      sStr.aData = Tcl_GetByteArrayFromObj(objv[2], &sStr.nData);
      sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);

      if( sStr.nStream ){
        rc = sqlite3rebaser_rebase_strm(p, 
            testStreamInput, (void*)&sStr,
            testStreamOutput, (void*)&sOut
        );
      }else{
        rc = sqlite3rebaser_rebase(p, sStr.nData, sStr.aData, &sOut.n, &sOut.p);
      }

      if( rc==SQLITE_OK ){
        Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n));
      }
      sqlite3_free(sOut.p);
      break;
    }
  }

  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }
  return TCL_OK;
}

static void SQLITE_TCLAPI test_rebaser_del(void *clientData){
  sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
  sqlite3rebaser_delete(p);
}

/*
** tclcmd: sqlite3rebaser_create NAME
*/
static int SQLITE_TCLAPI test_sqlite3rebaser_create(
  void * clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
  int rc;
  sqlite3_rebaser *pNew = 0;
  if( objc!=2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "NAME");
    return SQLITE_ERROR;
  }

  rc = sqlite3rebaser_create(&pNew);
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  Tcl_CreateObjCommand(interp, Tcl_GetString(objv[1]), test_rebaser_cmd,
      (ClientData)pNew, test_rebaser_del
  );
  Tcl_SetObjResult(interp, objv[1]);
  return TCL_OK;
}

int TestSession_Init(Tcl_Interp *interp){
  struct Cmd {
    const char *zCmd;
    Tcl_ObjCmdProc *xProc;
  } aCmd[] = {
    { "sqlite3session", test_sqlite3session },
    { "sqlite3session_foreach", test_sqlite3session_foreach },
    { "sqlite3changeset_invert", test_sqlite3changeset_invert },
    { "sqlite3changeset_concat", test_sqlite3changeset_concat },
    { "sqlite3changeset_apply", test_sqlite3changeset_apply },
    { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 },
    { "sqlite3changeset_apply_replace_all", 
      test_sqlite3changeset_apply_replace_all },
    { "sql_exec_changeset", test_sql_exec_changeset },
    { "sqlite3rebaser_create", test_sqlite3rebaser_create },
  };
  int i;

  for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
    struct Cmd *p = &aCmd[i];
    Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0);
  }

  return TCL_OK;
}

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