/ Check-in [cbedcb9a]
Login
SQLite training in Houston TX on 2019-11-05 (details)
Part of the 2019 Tcl Conference

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

Overview
Comment:Add the sqlite3changeset_start_v2() - a new version of _start() that accepts a flags parameter - and a streaming equivalent to the sessions module. Also add the SQLITE_CHANGESETSTART_INVERT flag, used with start_v2() to invert a changeset while iterating through it.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: cbedcb9aaefdfe00453efbdf0eac6c15e1f53bbe8fff2e7d534a5adf23be04f5
User & Date: dan 2018-10-20 13:48:09
Context
2018-10-23
13:48
Fix a problem with using window functions in compound (UNION, INTERSECT etc.) queries. check-in: 059ff53a user: dan tags: trunk
2018-10-20
13:48
Add the sqlite3changeset_start_v2() - a new version of _start() that accepts a flags parameter - and a streaming equivalent to the sessions module. Also add the SQLITE_CHANGESETSTART_INVERT flag, used with start_v2() to invert a changeset while iterating through it. check-in: cbedcb9a user: dan tags: trunk
2018-10-18
15:17
Take steps to avoid a potential integer overflow in sessionBufferGrow(). check-in: f7affa2e user: dan tags: trunk
Changes
Hide Diffs Unified Diffs Show Whitespace Changes Patch

Changes to ext/session/sessioninvert.test.

16
17
18
19
20
21
22






23
24
25
26
27
28
29
30
..
33
34
35
36
37
38
39




40
41
42
43
44
45
46
..
54
55
56
57
58
59
60


61
62
63
64
65
66

67
68
69
70
71











72
73
74
75
76
77
78







79
80
81
82
83
84
85
..
86
87
88
89
90
91
92


93
94
95
96



97
98
99
100
101
102
103
...
108
109
110
111
112
113
114




115
116
117
118
119
120
} 
source [file join [file dirname [info script]] session_common.tcl]
source $testdir/tester.tcl
ifcapable !session {finish_test; return}

set testprefix sessioninvert







proc do_invert_test {tn sql} {

  forcecopy test.db test.db2
  sqlite3 db2 test.db2

  set C [changeset_from_sql $sql]

  forcecopy test.db test.db3
................................................................................

  set I [sqlite3changeset_invert $C]
  sqlite3changeset_apply db $I {}
  uplevel [list do_test $tn.2 [list compare_db db db2] {}]
  
  sqlite3changeset_apply_v2 -invert db3 $C {}
  uplevel [list do_test $tn.3 [list compare_db db db3] {}]





  catch { db2 close }
  catch { db3 close }
}

do_execsql_test 1.0 {
  CREATE TABLE t1(a PRIMARY KEY, b, c);
................................................................................
  INSERT INTO t1 VALUES(6, 'six', 'vi');

  INSERT INTO t2 SELECT * FROM t1;
}

do_invert_test 1.1 {
  INSERT INTO t1 VALUES(7, 'seven', 'vii');


}

do_invert_test 1.2 {
  DELETE FROM t1 WHERE a<4;
}


do_invert_test 1.2 {
  UPDATE t1 SET c=5;
}

do_invert_test 1.3 {











  UPDATE t1 SET b = a+1 WHERE a%2;
  DELETE FROM t2;
  INSERT INTO t1 VALUES(10, 'ten', NULL);
}

do_invert_test 1.4 {
  UPDATE t2 SET d = d-1;







}

do_execsql_test 2.0 { 
  ANALYZE;
  PRAGMA writable_schema = 1;
  DROP TABLE IF EXISTS sqlite_stat4;
  SELECT * FROM sqlite_stat1;
................................................................................
} {
  t2 sqlite_autoindex_t2_1 {6 1 1} 
  t1 sqlite_autoindex_t1_1 {6 1}
}

do_invert_test 2.1 {
  INSERT INTO sqlite_stat1 VALUES('t3', 'idx2', '1 2 3');


}

do_invert_test 2.2 {
  DELETE FROM sqlite_stat1;



}

do_invert_test 2.3 {
  UPDATE sqlite_stat1 SET stat = 'hello world';
}

do_test 3.0 {
................................................................................
    DELETE FROM t2 WHERE d = 3;
  }]

  list [catch { sqlite3changeset_apply_v2 -invert db2 $P {} } msg] $msg
} {1 SQLITE_CORRUPT}

do_test 3.1 {




  sqlite3changeset_apply_v2 db2 $P {} 
  compare_db db db2
} {}


finish_test







>
>
>
>
>
>
|







 







>
>
>
>







 







>
>




|
<
>
|
|



>
>
>
>
>
>
>
>
>
>
>





|

>
>
>
>
>
>
>







 







>
>




>
>
>







 







>
>
>
>






16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
..
64
65
66
67
68
69
70
71
72
73
74
75
76
77

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
...
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
...
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
} 
source [file join [file dirname [info script]] session_common.tcl]
source $testdir/tester.tcl
ifcapable !session {finish_test; return}

set testprefix sessioninvert

proc iter_invert {C} {
  set x [list]
  sqlite3session_foreach -invert c $C { lappend x $c }
  set x
}

proc do_invert_test {tn sql {iter {}}} {

  forcecopy test.db test.db2
  sqlite3 db2 test.db2

  set C [changeset_from_sql $sql]

  forcecopy test.db test.db3
................................................................................

  set I [sqlite3changeset_invert $C]
  sqlite3changeset_apply db $I {}
  uplevel [list do_test $tn.2 [list compare_db db db2] {}]
  
  sqlite3changeset_apply_v2 -invert db3 $C {}
  uplevel [list do_test $tn.3 [list compare_db db db3] {}]

  if {$iter!=""} {
    uplevel [list do_test $tn.4 [list iter_invert $C] [list {*}$iter]]
  }

  catch { db2 close }
  catch { db3 close }
}

do_execsql_test 1.0 {
  CREATE TABLE t1(a PRIMARY KEY, b, c);
................................................................................
  INSERT INTO t1 VALUES(6, 'six', 'vi');

  INSERT INTO t2 SELECT * FROM t1;
}

do_invert_test 1.1 {
  INSERT INTO t1 VALUES(7, 'seven', 'vii');
} {
  {DELETE t1 0 X.. {i 7 t seven t vii} {}}
}

do_invert_test 1.2 {
  DELETE FROM t1 WHERE a<4;
} {

  {INSERT t1 0 X.. {} {i 1 t one t i}}
  {INSERT t1 0 X.. {} {i 2 t two t ii}}
  {INSERT t1 0 X.. {} {i 3 t three t iii}}
}

do_invert_test 1.3 {
  UPDATE t1 SET c=5;
} {
  {UPDATE t1 0 X.. {i 1 {} {} i 5} {{} {} {} {} t i}}
  {UPDATE t1 0 X.. {i 2 {} {} i 5} {{} {} {} {} t ii}}
  {UPDATE t1 0 X.. {i 3 {} {} i 5} {{} {} {} {} t iii}}
  {UPDATE t1 0 X.. {i 4 {} {} i 5} {{} {} {} {} t iv}}
  {UPDATE t1 0 X.. {i 5 {} {} i 5} {{} {} {} {} t v}}
  {UPDATE t1 0 X.. {i 6 {} {} i 5} {{} {} {} {} t vi}}
}

do_invert_test 1.4 {
  UPDATE t1 SET b = a+1 WHERE a%2;
  DELETE FROM t2;
  INSERT INTO t1 VALUES(10, 'ten', NULL);
}

do_invert_test 1.5 {
  UPDATE t2 SET d = d-1;
} {
  {UPDATE t2 0 .XX {i 2 t three t iii} {i 3 {} {} {} {}}}
  {UPDATE t2 0 .XX {i 1 t two t ii} {i 2 {} {} {} {}}}
  {UPDATE t2 0 .XX {i 5 t six t vi} {i 6 {} {} {} {}}}
  {UPDATE t2 0 .XX {i 3 t four t iv} {i 4 {} {} {} {}}}
  {UPDATE t2 0 .XX {i 0 t one t i} {i 1 {} {} {} {}}}
  {UPDATE t2 0 .XX {i 4 t five t v} {i 5 {} {} {} {}}}
}

do_execsql_test 2.0 { 
  ANALYZE;
  PRAGMA writable_schema = 1;
  DROP TABLE IF EXISTS sqlite_stat4;
  SELECT * FROM sqlite_stat1;
................................................................................
} {
  t2 sqlite_autoindex_t2_1 {6 1 1} 
  t1 sqlite_autoindex_t1_1 {6 1}
}

do_invert_test 2.1 {
  INSERT INTO sqlite_stat1 VALUES('t3', 'idx2', '1 2 3');
} {
  {DELETE sqlite_stat1 0 XX. {t t3 t idx2 t {1 2 3}} {}}
}

do_invert_test 2.2 {
  DELETE FROM sqlite_stat1;
} {
  {INSERT sqlite_stat1 0 XX. {} {t t1 t sqlite_autoindex_t1_1 t {6 1}}}
  {INSERT sqlite_stat1 0 XX. {} {t t2 t sqlite_autoindex_t2_1 t {6 1 1}}}
}

do_invert_test 2.3 {
  UPDATE sqlite_stat1 SET stat = 'hello world';
}

do_test 3.0 {
................................................................................
    DELETE FROM t2 WHERE d = 3;
  }]

  list [catch { sqlite3changeset_apply_v2 -invert db2 $P {} } msg] $msg
} {1 SQLITE_CORRUPT}

do_test 3.1 {
  list [catch { sqlite3session_foreach -invert db2 $P {} } msg] $msg
} {1 SQLITE_CORRUPT}

do_test 3.2 {
  sqlite3changeset_apply_v2 db2 $P {} 
  compare_db db db2
} {}


finish_test

Changes to ext/session/sqlite3session.c.

2575
2576
2577
2578
2579
2580
2581









2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592









2593
2594
2595
2596
2597
2598
2599
int sqlite3changeset_start(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int nChangeset,                 /* Size of buffer pChangeset in bytes */
  void *pChangeset                /* Pointer to buffer containing changeset */
){
  return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset, 0);
}










/*
** Streaming version of sqlite3changeset_start().
*/
int sqlite3changeset_start_strm(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn
){
  return sessionChangesetStart(pp, xInput, pIn, 0, 0, 0);
}










/*
** If the SessionInput object passed as the only argument is a streaming
** object and the buffer is full, discard some data to free up space.
*/
static void sessionDiscardData(SessionInput *pIn){
  if( pIn->xInput && pIn->iNext>=SESSIONS_STRM_CHUNK_SIZE ){







>
>
>
>
>
>
>
>
>











>
>
>
>
>
>
>
>
>







2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
int sqlite3changeset_start(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int nChangeset,                 /* Size of buffer pChangeset in bytes */
  void *pChangeset                /* Pointer to buffer containing changeset */
){
  return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset, 0);
}
int sqlite3changeset_start_v2(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int nChangeset,                 /* Size of buffer pChangeset in bytes */
  void *pChangeset,               /* Pointer to buffer containing changeset */
  int flags
){
  int bInvert = !!(flags & SQLITE_CHANGESETSTART_INVERT);
  return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset, bInvert);
}

/*
** Streaming version of sqlite3changeset_start().
*/
int sqlite3changeset_start_strm(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn
){
  return sessionChangesetStart(pp, xInput, pIn, 0, 0, 0);
}
int sqlite3changeset_start_v2_strm(
  sqlite3_changeset_iter **pp,    /* OUT: Changeset iterator handle */
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn,
  int flags
){
  int bInvert = !!(flags & SQLITE_CHANGESETSTART_INVERT);
  return sessionChangesetStart(pp, xInput, pIn, 0, 0, bInvert);
}

/*
** If the SessionInput object passed as the only argument is a streaming
** object and the buffer is full, discard some data to free up space.
*/
static void sessionDiscardData(SessionInput *pIn){
  if( pIn->xInput && pIn->iNext>=SESSIONS_STRM_CHUNK_SIZE ){

Changes to ext/session/sqlite3session.h.

469
470
471
472
473
474
475







476
477
478
479
480
481



















482
483
484
485
486
487
488
....
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
....
1547
1548
1549
1550
1551
1552
1553






1554
1555
1556
1557
1558
1559
1560
** [sqlite3changeset_invert()] functions, all changes within the changeset 
** that apply to a single table are grouped together. This means that when 
** an application iterates through a changeset using an iterator created by 
** this function, all changes that relate to a single table are visited 
** consecutively. There is no chance that the iterator will visit a change 
** the applies to table X, then one for table Y, and then later on visit 
** another change for table X.







*/
int sqlite3changeset_start(
  sqlite3_changeset_iter **pp,    /* OUT: New changeset iterator handle */
  int nChangeset,                 /* Size of changeset blob in bytes */
  void *pChangeset                /* Pointer to blob containing changeset */
);





















/*
** CAPI3REF: Advance A Changeset Iterator
** METHOD: sqlite3_changeset_iter
**
** This function may only be used with iterators created by function
................................................................................
  int(*xConflict)(
    void *pCtx,                   /* Copy of sixth arg to _apply() */
    int eConflict,                /* DATA, MISSING, CONFLICT, CONSTRAINT */
    sqlite3_changeset_iter *p     /* Handle describing change and conflict */
  ),
  void *pCtx,                     /* First argument passed to xConflict */
  void **ppRebase, int *pnRebase, /* OUT: Rebase data */
  int flags                       /* Combination of SESSION_APPLY_* flags */
);

/*
** CAPI3REF: Flags for sqlite3changeset_apply_v2
**
** The following flags may passed via the 9th parameter to
** [sqlite3changeset_apply_v2] and [sqlite3changeset_apply_v2_strm]:
................................................................................
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut
);
int sqlite3changeset_start_strm(
  sqlite3_changeset_iter **pp,
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn






);
int sqlite3session_changeset_strm(
  sqlite3_session *pSession,
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut
);
int sqlite3session_patchset_strm(







>
>
>
>
>
>
>






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







 







|







 







>
>
>
>
>
>







469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
....
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
....
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
** [sqlite3changeset_invert()] functions, all changes within the changeset 
** that apply to a single table are grouped together. This means that when 
** an application iterates through a changeset using an iterator created by 
** this function, all changes that relate to a single table are visited 
** consecutively. There is no chance that the iterator will visit a change 
** the applies to table X, then one for table Y, and then later on visit 
** another change for table X.
**
** The behavior of sqlite3changeset_start_v2() and its streaming equivalent
** may be modified by passing a combination of
** [SQLITE_CHANGESETSTART_INVERT | supported flags] as the 4th parameter.
**
** Note that the sqlite3changeset_start_v2() API is still <b>experimental</b>
** and therefore subject to change.
*/
int sqlite3changeset_start(
  sqlite3_changeset_iter **pp,    /* OUT: New changeset iterator handle */
  int nChangeset,                 /* Size of changeset blob in bytes */
  void *pChangeset                /* Pointer to blob containing changeset */
);
int sqlite3changeset_start_v2(
  sqlite3_changeset_iter **pp,    /* OUT: New changeset iterator handle */
  int nChangeset,                 /* Size of changeset blob in bytes */
  void *pChangeset,               /* Pointer to blob containing changeset */
  int flags                       /* SESSION_CHANGESETSTART_* flags */
);

/*
** CAPI3REF: Flags for sqlite3changeset_start_v2
**
** The following flags may passed via the 4th parameter to
** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]:
**
** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd>
**   Invert the changeset while iterating through it. This is equivalent to
**   inverting a changeset using sqlite3changeset_invert() before applying it.
**   It is an error to specify this flag with a patchset.
*/
#define SQLITE_CHANGESETSTART_INVERT        0x0002


/*
** CAPI3REF: Advance A Changeset Iterator
** METHOD: sqlite3_changeset_iter
**
** This function may only be used with iterators created by function
................................................................................
  int(*xConflict)(
    void *pCtx,                   /* Copy of sixth arg to _apply() */
    int eConflict,                /* DATA, MISSING, CONFLICT, CONSTRAINT */
    sqlite3_changeset_iter *p     /* Handle describing change and conflict */
  ),
  void *pCtx,                     /* First argument passed to xConflict */
  void **ppRebase, int *pnRebase, /* OUT: Rebase data */
  int flags                       /* SESSION_CHANGESETAPPLY_* flags */
);

/*
** CAPI3REF: Flags for sqlite3changeset_apply_v2
**
** The following flags may passed via the 9th parameter to
** [sqlite3changeset_apply_v2] and [sqlite3changeset_apply_v2_strm]:
................................................................................
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut
);
int sqlite3changeset_start_strm(
  sqlite3_changeset_iter **pp,
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn
);
int sqlite3changeset_start_v2_strm(
  sqlite3_changeset_iter **pp,
  int (*xInput)(void *pIn, void *pData, int *pnData),
  void *pIn,
  int flags
);
int sqlite3session_changeset_strm(
  sqlite3_session *pSession,
  int (*xOutput)(void *pOut, const void *pData, int nData),
  void *pOut
);
int sqlite3session_patchset_strm(

Changes to ext/session/test_session.c.

977
978
979
980
981
982
983

984
985
986
987
988
989






990


991



992
993

994
995
996
997
998
999
1000
1001
1002











1003
1004
1005
1006
1007
1008
1009

1010
1011
1012
1013
1014
1015
1016
  int nChangeset;
  sqlite3_changeset_iter *pIter;
  int rc;
  Tcl_Obj *pVarname;
  Tcl_Obj *pCS;
  Tcl_Obj *pScript;
  int isCheckNext = 0;


  TestStreamInput sStr;
  memset(&sStr, 0, sizeof(sStr));

  if( objc>1 ){
    char *zOpt = Tcl_GetString(objv[1]);






    isCheckNext = (strcmp(zOpt, "-next")==0);


  }



  if( objc!=4+isCheckNext ){
    Tcl_WrongNumArgs(interp, 1, objv, "?-next? VARNAME CHANGESET SCRIPT");

    return TCL_ERROR;
  }

  pVarname = objv[1+isCheckNext];
  pCS = objv[2+isCheckNext];
  pScript = objv[3+isCheckNext];

  pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset);
  sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);











  if( sStr.nStream==0 ){
    rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
  }else{
    sStr.aData = (unsigned char*)pChangeset;
    sStr.nData = nChangeset;
    rc = sqlite3changeset_start_strm(&pIter, testStreamInput, (void*)&sStr);
  }

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

  while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
    int nCol;                     /* Number of columns in table */
    int nCol2;                    /* Number of columns in table */







>




|

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



|
|
|



>
>
>
>
>
>
>
>
>
>
>







>







977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
  int nChangeset;
  sqlite3_changeset_iter *pIter;
  int rc;
  Tcl_Obj *pVarname;
  Tcl_Obj *pCS;
  Tcl_Obj *pScript;
  int isCheckNext = 0;
  int isInvert = 0;

  TestStreamInput sStr;
  memset(&sStr, 0, sizeof(sStr));

  while( objc>1 ){
    char *zOpt = Tcl_GetString(objv[1]);
    int nOpt = strlen(zOpt);
    if( zOpt[0]!='-' ) break;
    if( nOpt<=7 && 0==sqlite3_strnicmp(zOpt, "-invert", nOpt) ){
      isInvert = 1;
    }else
    if( nOpt<=5 && 0==sqlite3_strnicmp(zOpt, "-next", nOpt) ){
      isCheckNext = 1;
    }else{
      break;
    }
    objv++;
    objc--;
  }
  if( objc!=4 ){
    Tcl_WrongNumArgs(
        interp, 1, objv, "?-next? ?-invert? VARNAME CHANGESET SCRIPT");
    return TCL_ERROR;
  }

  pVarname = objv[1];
  pCS = objv[2];
  pScript = objv[3];

  pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset);
  sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
  if( isInvert ){
    int f = SQLITE_CHANGESETSTART_INVERT;
    if( sStr.nStream==0 ){
      rc = sqlite3changeset_start_v2(&pIter, nChangeset, pChangeset, f);
    }else{
      void *pCtx = (void*)&sStr;
      sStr.aData = (unsigned char*)pChangeset;
      sStr.nData = nChangeset;
      rc = sqlite3changeset_start_v2_strm(&pIter, testStreamInput, pCtx, f);
    }
  }else{
    if( sStr.nStream==0 ){
      rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
    }else{
      sStr.aData = (unsigned char*)pChangeset;
      sStr.nData = nChangeset;
      rc = sqlite3changeset_start_strm(&pIter, testStreamInput, (void*)&sStr);
    }
  }
  if( rc!=SQLITE_OK ){
    return test_session_error(interp, rc, 0);
  }

  while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
    int nCol;                     /* Number of columns in table */
    int nCol2;                    /* Number of columns in table */