Fossil

Check-in [22c3354d]
Login

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

Overview
Comment:Add the ability to transfer subscriber information using the "fossil config sync alert" command.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 22c3354dcbb718927396250ca32e2579b96e9fa1de1b629fd94786d689aeb7ea
User & Date: drh 2018-06-25 15:56:20.241
Context
2018-06-25
16:14
Enhance the subscriber since to include the creation time and so that a new random subscriber code is set. ... (check-in: 7e87699d user: drh tags: trunk)
15:56
Add the ability to transfer subscriber information using the "fossil config sync alert" command. ... (check-in: 22c3354d user: drh tags: trunk)
14:45
Enhancements to administrator access to the subscriber list. ... (check-in: b7b877ef user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/configure.c.
34
35
36
37
38
39
40


41
42
43
44
45
46
47
48
49
#define CONFIGSET_TKT       0x000004     /* Ticket configuration */
#define CONFIGSET_PROJ      0x000008     /* Project name */
#define CONFIGSET_SHUN      0x000010     /* Shun settings */
#define CONFIGSET_USER      0x000020     /* The USER table */
#define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
#define CONFIGSET_XFER      0x000080     /* Transfer configuration */
#define CONFIGSET_ALIAS     0x000100     /* URL Aliases */



#define CONFIGSET_ALL       0x0001ff     /* Everything */

#define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */

/*
** This mask is used for the common TH1 configuration settings (i.e. those
** that are not specific to one particular subsystem, such as the transfer
** subsystem).







>
>

|







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#define CONFIGSET_TKT       0x000004     /* Ticket configuration */
#define CONFIGSET_PROJ      0x000008     /* Project name */
#define CONFIGSET_SHUN      0x000010     /* Shun settings */
#define CONFIGSET_USER      0x000020     /* The USER table */
#define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
#define CONFIGSET_XFER      0x000080     /* Transfer configuration */
#define CONFIGSET_ALIAS     0x000100     /* URL Aliases */
#define CONFIGSET_ALERT     0x000200     /* Email alerts */
#define CONFIGSET_FORUM     0x000400     /* Forum posts */

#define CONFIGSET_ALL       0x0007ff     /* Everything */

#define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */

/*
** This mask is used for the common TH1 configuration settings (i.e. those
** that are not specific to one particular subsystem, such as the transfer
** subsystem).
66
67
68
69
70
71
72


73
74
75
76
77
78
79
                                      "Web interface appearance settings"    },
  { "/css",          CONFIGSET_CSS,   "Style sheet"                          },
  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
  { "/xfer",         CONFIGSET_XFER,  "Transfer setup",                      },
  { "/alias",        CONFIGSET_ALIAS, "URL Aliases",                         },


  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
};


/*
** The following is a list of settings that we are willing to
** transfer.







>
>







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
                                      "Web interface appearance settings"    },
  { "/css",          CONFIGSET_CSS,   "Style sheet"                          },
  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
  { "/xfer",         CONFIGSET_XFER,  "Transfer setup",                      },
  { "/alias",        CONFIGSET_ALIAS, "URL Aliases",                         },
  { "/alert",        CONFIGSET_ALERT, "Notification sent by email",          },
  { "/forum",        CONFIGSET_FORUM, "Forum posts",                         },
  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
};


/*
** The following is a list of settings that we are willing to
** transfer.
155
156
157
158
159
160
161


162
163
164
165
166
167
168

  { "@concealed",             CONFIGSET_ADDR },

  { "@shun",                  CONFIGSET_SHUN },

  { "@alias",                 CONFIGSET_ALIAS },



  { "xfer-common-script",     CONFIGSET_XFER },
  { "xfer-push-script",       CONFIGSET_XFER },
  { "xfer-commit-script",     CONFIGSET_XFER },
  { "xfer-ticket-script",     CONFIGSET_XFER },

};
static int iConfig = 0;







>
>







159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

  { "@concealed",             CONFIGSET_ADDR },

  { "@shun",                  CONFIGSET_SHUN },

  { "@alias",                 CONFIGSET_ALIAS },

  { "@subscriber",            CONFIGSET_ALERT },

  { "xfer-common-script",     CONFIGSET_XFER },
  { "xfer-push-script",       CONFIGSET_XFER },
  { "xfer-commit-script",     CONFIGSET_XFER },
  { "xfer-ticket-script",     CONFIGSET_XFER },

};
static int iConfig = 0;
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
  return blob_sql_text(&x);
}

/*
** Return the mask for the named configuration parameter if it can be
** safely exported.  Return 0 if the parameter is not safe to export.
**
** "Safe" in the previous paragraph means the permission is created to
** export the property.  In other words, the requesting side has presented
** login credentials and has sufficient capabilities to access the requested
** information.
*/
int configure_is_exportable(const char *zName){
  int i;
  int n = strlen(zName);
  if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){
    zName++;
    n -= 2;
  }
  for(i=0; i<count(aConfig); i++){
    if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
      int m = aConfig[i].groupMask;
      if( !g.perm.Admin ){
        m &= ~CONFIGSET_USER;



      }
      if( !g.perm.RdAddr ){
        m &= ~CONFIGSET_ADDR;
      }
      return m;
    }
  }







|















|
>
>
>







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
  return blob_sql_text(&x);
}

/*
** Return the mask for the named configuration parameter if it can be
** safely exported.  Return 0 if the parameter is not safe to export.
**
** "Safe" in the previous paragraph means the permission is granted to
** export the property.  In other words, the requesting side has presented
** login credentials and has sufficient capabilities to access the requested
** information.
*/
int configure_is_exportable(const char *zName){
  int i;
  int n = strlen(zName);
  if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){
    zName++;
    n -= 2;
  }
  for(i=0; i<count(aConfig); i++){
    if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
      int m = aConfig[i].groupMask;
      if( !g.perm.Admin ){
        m &= ~(CONFIGSET_USER|CONFIGSET_ALERT);
      }
      if( !g.perm.RdForum ){
        m &= ~(CONFIGSET_FORUM);
      }
      if( !g.perm.RdAddr ){
        m &= ~CONFIGSET_ADDR;
      }
      return m;
    }
  }
310
311
312
313
314
315
316
317




318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356



357
358
359
360
361
362
363
364
365
366

367
368
369
370
371
372
373
374


375
376
377
378
379
380
381
** sync session.
**
** Mask consists of one or more CONFIGSET_* values ORed together, to
** designate what types of configuration we are allowed to receive.
**
** NEW FORMAT:
**
** zName is one of "/config", "/user", "/shun", "/reportfmt", or "/concealed".




** zName indicates the table that holds the configuration information being
** transferred.  pContent is a string that consist of alternating Fossil
** and SQL tokens.  The First token is a timestamp in seconds since 1970.
** The second token is a primary key for the table identified by zName.  If
** The entry with the corresponding primary key exists and has a more recent
** mtime, then nothing happens.  If the entry does not exist or if it has
** an older mtime, then the content described by subsequent token pairs is
** inserted.  The first element of each token pair is a column name and
** the second is its value.
**
** In overview, we have:
**
**    NAME        CONTENT
**    -------     -----------------------------------------------------------
**    /config     $MTIME $NAME value $VALUE
**    /user       $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE
**    /shun       $MTIME $UUID scom $VALUE
**    /reportfmt  $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE
**    /concealed  $MTIME $HASH content $VALUE
**
** OLD FORMAT:
**
** The old format is retained for backwards compatibility, but is deprecated.
** The cutover from old format to new was on 2011-04-25.  After sufficient
** time has passed, support for the old format will be removed.
** Update: Support for the old format was removed on 2017-09-20.
**
** zName is either the NAME of an element of the CONFIG table, or else
** one of the special names "@shun", "@reportfmt", "@user", or "@concealed".
** If zName is a CONFIG table name, then CONTENT replaces (overwrites) the
** element in the CONFIG table.  For one of the @-labels, CONTENT is raw
** SQL that is evaluated.  Note that the raw SQL in CONTENT might not
** insert directly into the target table but might instead use a proxy
** table like _fer_reportfmt or _xfer_user.  Such tables must be created
** ahead of time using configure_prepare_to_receive().  Then after multiple
** calls to this routine, configure_finalize_receive() to transfer the
** information received into the true target table.
*/
void configure_receive(const char *zName, Blob *pContent, int groupMask){



  if( zName[0]=='/' ){
    /* The new format */
    char *azToken[12];
    int nToken = 0;
    int ii, jj;
    int thisMask;
    Blob name, value, sql;
    static const struct receiveType {
      const char *zName;
      const char *zPrimKey;

      int nField;
      const char *azField[4];
    } aType[] = {
      { "/config",    "name",  1, { "value", 0, 0, 0 }              },
      { "@user",      "login", 4, { "pw", "cap", "info", "photo" }  },
      { "@shun",      "uuid",  1, { "scom", 0, 0, 0 }               },
      { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } },
      { "@concealed", "hash",  1, { "content", 0, 0, 0 }            },


    };
    for(ii=0; ii<count(aType); ii++){
      if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
    }
    if( ii>=count(aType) ) return;
    while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
      char *z = blob_terminate(&name);







|
>
>
>
>



















|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


>
>
>








|
|
>
|
|

|
|
|
|
|
>
>







319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350

















351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
** sync session.
**
** Mask consists of one or more CONFIGSET_* values ORed together, to
** designate what types of configuration we are allowed to receive.
**
** NEW FORMAT:
**
** zName is one of:
**
**     "/config", "/user",  "/shun", "/reportfmt", "/concealed",
**     "/alert", "/forum"
**
** zName indicates the table that holds the configuration information being
** transferred.  pContent is a string that consist of alternating Fossil
** and SQL tokens.  The First token is a timestamp in seconds since 1970.
** The second token is a primary key for the table identified by zName.  If
** The entry with the corresponding primary key exists and has a more recent
** mtime, then nothing happens.  If the entry does not exist or if it has
** an older mtime, then the content described by subsequent token pairs is
** inserted.  The first element of each token pair is a column name and
** the second is its value.
**
** In overview, we have:
**
**    NAME        CONTENT
**    -------     -----------------------------------------------------------
**    /config     $MTIME $NAME value $VALUE
**    /user       $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE
**    /shun       $MTIME $UUID scom $VALUE
**    /reportfmt  $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE
**    /concealed  $MTIME $HASH content $VALUE
**    /subscriber $SMTIME $SEMAIL suname $V sdigest $V sdonotcall $V ssub $V

















*/
void configure_receive(const char *zName, Blob *pContent, int groupMask){
  int checkMask;   /* Masks for which we must first check existance of tables */

  checkMask = CONFIGSET_ALERT;
  if( zName[0]=='/' ){
    /* The new format */
    char *azToken[12];
    int nToken = 0;
    int ii, jj;
    int thisMask;
    Blob name, value, sql;
    static const struct receiveType {
      const char *zName;         /* Configuration key for this table */
      const char *zPrimKey;      /* Primary key column */
      const char *zMTime;        /* Column holding the mtime */
      int nField;                /* Number of data fields */
      const char *azField[4];    /* Names of the data fields */
    } aType[] = {
      { "/config",    "name",  "mtime", 1, { "value", 0, 0, 0 }             },
      { "@user",      "login", "mtime", 4, { "pw", "cap", "info", "photo" }  },
      { "@shun",      "uuid",  "mtime", 1, { "scom", 0, 0, 0 }               },
      { "@reportfmt", "title", "mtime", 3, { "owner", "cols", "sqlcode", 0 } },
      { "@concealed", "hash",  "mtime", 1, { "content", 0, 0, 0 }            },
      { "@subscriber","semail","smtime",4, { "suname","sdigest",
                                             "sdonotcall","ssub"}            },
    };
    for(ii=0; ii<count(aType); ii++){
      if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
    }
    if( ii>=count(aType) ) return;
    while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
      char *z = blob_terminate(&name);
396
397
398
399
400
401
402







403
404
405
406
407
408
409
410
411
412
413
414

415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
    if( nToken<2 ) return;
    if( aType[ii].zName[0]=='/' ){
      thisMask = configure_is_exportable(azToken[1]);
    }else{
      thisMask = configure_is_exportable(aType[ii].zName);
    }
    if( (thisMask & groupMask)==0 ) return;








    blob_zero(&sql);
    if( groupMask & CONFIGSET_OVERWRITE ){
      if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
        db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]);
        configHasBeenReset |= thisMask;
      }
      blob_append_sql(&sql, "REPLACE INTO ");
    }else{
      blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
    }
    blob_append_sql(&sql, "\"%w\"(\"%w\", mtime", &zName[1], aType[ii].zPrimKey);

    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
    }
    blob_append_sql(&sql,") VALUES(%s,%s",
       azToken[1] /*safe-for-%s*/, azToken[0] /*safe-for-%s*/);
    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
    }
    db_multi_exec("%s)", blob_sql_text(&sql));
    if( db_changes()==0 ){
      blob_reset(&sql);
      blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
                      &zName[1], azToken[0]/*safe-for-%s*/);
      for(jj=2; jj<nToken; jj+=2){
        blob_append_sql(&sql, ", \"%w\"=%s",
                        azToken[jj], azToken[jj+1]/*safe-for-%s*/);
      }
      blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s",
                   aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
                   azToken[0]/*safe-for-%s*/);
      db_multi_exec("%s", blob_sql_text(&sql));
    }
    blob_reset(&sql);
    rebuildMask |= thisMask;
  }
}








>
>
>
>
>
>
>











|
>











|
|




|

|







398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
    if( nToken<2 ) return;
    if( aType[ii].zName[0]=='/' ){
      thisMask = configure_is_exportable(azToken[1]);
    }else{
      thisMask = configure_is_exportable(aType[ii].zName);
    }
    if( (thisMask & groupMask)==0 ) return;
    if( (thisMask & checkMask)!=0 ){
      if( (thisMask & CONFIGSET_ALERT)!=0 ){
        email_schema(1);
      }
      checkMask &= ~thisMask;
    }
     

    blob_zero(&sql);
    if( groupMask & CONFIGSET_OVERWRITE ){
      if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
        db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]);
        configHasBeenReset |= thisMask;
      }
      blob_append_sql(&sql, "REPLACE INTO ");
    }else{
      blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
    }
    blob_append_sql(&sql, "\"%w\"(\"%w\", \"%w\"",
         &zName[1], aType[ii].zPrimKey, aType[ii].zMTime);
    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
    }
    blob_append_sql(&sql,") VALUES(%s,%s",
       azToken[1] /*safe-for-%s*/, azToken[0] /*safe-for-%s*/);
    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
    }
    db_multi_exec("%s)", blob_sql_text(&sql));
    if( db_changes()==0 ){
      blob_reset(&sql);
      blob_append_sql(&sql, "UPDATE \"%w\" SET \"%w\"=%s",
                      &zName[1], aType[ii].zMTime, azToken[0]/*safe-for-%s*/);
      for(jj=2; jj<nToken; jj+=2){
        blob_append_sql(&sql, ", \"%w\"=%s",
                        azToken[jj], azToken[jj+1]/*safe-for-%s*/);
      }
      blob_append_sql(&sql, " WHERE \"%w\"=%s AND \"%w\"<%s",
                   aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
                   aType[ii].zMTime, azToken[0]/*safe-for-%s*/);
      db_multi_exec("%s", blob_sql_text(&sql));
    }
    blob_reset(&sql);
    rebuildMask |= thisMask;
  }
}

571
572
573
574
575
576
577
























578
579
580
581
582
583
584
      );
      blob_appendf(pOut, "config /config %d\n%s\n",
                   blob_size(&rec), blob_str(&rec));
      nCard++;
      blob_reset(&rec);
    }
    db_finalize(&q);
























  }
  db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
                 " WHERE name=:name AND mtime>=%lld", iStart);
  for(ii=0; ii<count(aConfig); ii++){
    if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
      db_bind_text(&q, ":name", aConfig[ii].zName);
      while( db_step(&q)==SQLITE_ROW ){







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







581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
      );
      blob_appendf(pOut, "config /config %d\n%s\n",
                   blob_size(&rec), blob_str(&rec));
      nCard++;
      blob_reset(&rec);
    }
    db_finalize(&q);
  }
  if( (groupMask & CONFIGSET_ALERT)!=0
   && db_table_exists("repository","subscriber")
  ){
    db_prepare(&q, "SELECT (smtime-2440587.5)*86400,"
                   " quote(semail), quote(suname), quote(sdigest),"
                   " quote(sdonotcall), quote(ssub)"
                   " FROM subscriber WHERE sverified"
                   " AND (smtime-2440587.5)*86400>=%lld", iStart);
    while( db_step(&q)==SQLITE_ROW ){
      blob_appendf(&rec,"%lld %s suname %s sdigest %s sdonotcall %s ssub %s",
        db_column_int64(&q, 0), /* smtime */
        db_column_text(&q, 1),  /* semail (PK) */
        db_column_text(&q, 2),  /* suname */
        db_column_text(&q, 3),  /* sdigest */
        db_column_text(&q, 4),  /* sdonotcall */
        db_column_text(&q, 5)   /* ssub */
      );
      blob_appendf(pOut, "config /subscriber %d\n%s\n",
                   blob_size(&rec), blob_str(&rec));
      nCard++;
      blob_reset(&rec);
    }
    db_finalize(&q);
  }
  db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
                 " WHERE name=:name AND mtime>=%lld", iStart);
  for(ii=0; ii<count(aConfig); ii++){
    if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
      db_bind_text(&q, ":name", aConfig[ii].zName);
      while( db_step(&q)==SQLITE_ROW ){
790
791
792
793
794
795
796









797
798
799
800
801
802
803
      }else if( fossil_strcmp(zName,"@user")==0 ){
        db_multi_exec("DELETE FROM user");
        db_create_default_users(0, 0);
      }else if( fossil_strcmp(zName,"@concealed")==0 ){
        db_multi_exec("DELETE FROM concealed");
      }else if( fossil_strcmp(zName,"@shun")==0 ){
        db_multi_exec("DELETE FROM shun");









      }else if( fossil_strcmp(zName,"@reportfmt")==0 ){
        db_multi_exec("DELETE FROM reportfmt");
        assert( strchr(zRepositorySchemaDefaultReports,'%')==0 );
        db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/);
      }
    }
    db_end_transaction(0);







>
>
>
>
>
>
>
>
>







824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
      }else if( fossil_strcmp(zName,"@user")==0 ){
        db_multi_exec("DELETE FROM user");
        db_create_default_users(0, 0);
      }else if( fossil_strcmp(zName,"@concealed")==0 ){
        db_multi_exec("DELETE FROM concealed");
      }else if( fossil_strcmp(zName,"@shun")==0 ){
        db_multi_exec("DELETE FROM shun");
      }else if( fossil_strcmp(zName,"@alert")==0 ){
        if( db_table_exists("repository","subscriber") ){
          db_multi_exec("DELETE FROM subscriber");
        }
      }else if( fossil_strcmp(zName,"@forum")==0 ){
        if( db_table_exists("repository","forumpost") ){
          db_multi_exec("DELETE FROM forumpost");
          db_multi_exec("DELETE FROM forumthread");
        }
      }else if( fossil_strcmp(zName,"@reportfmt")==0 ){
        db_multi_exec("DELETE FROM reportfmt");
        assert( strchr(zRepositorySchemaDefaultReports,'%')==0 );
        db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/);
      }
    }
    db_end_transaction(0);