sqlite/ext/session/changeset.c
drh 03168cacd5 A reasonably complete implementation of the "changeset" command-line tool
and the ".sessions" command in the command-line shell.

FossilOrigin-Name: 7b12f1f9c012f33d376242920583807b014b3287
2014-08-18 20:01:31 +00:00

413 lines
12 KiB
C

/*
** 2014-08-18
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
** This file contains code to implement the "changeset" command line
** utility for displaying and transforming changesets generated by
** the Sessions extension.
*/
#include "sqlite3.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
/*
** Show a usage message on stderr then quit.
*/
static void usage(const char *argv0){
fprintf(stderr, "Usage: %s FILENAME COMMAND ...\n", argv0);
fprintf(stderr,
"COMMANDs:\n"
" apply DB Apply the changeset to database file DB\n"
" concat FILE2 OUT Concatenate FILENAME and FILE2 into OUT\n"
" dump Show the complete content of the changeset\n"
" invert OUT Write an inverted changeset into file OUT\n"
" sql Give a pseudo-SQL rendering of the changeset\n"
);
exit(1);
}
/*
** Read the content of a disk file into an in-memory buffer
*/
static void readFile(const char *zFilename, int *pSz, void **ppBuf){
FILE *f;
int sz;
void *pBuf;
f = fopen(zFilename, "rb");
if( f==0 ){
fprintf(stderr, "cannot open \"%s\" for reading\n", zFilename);
exit(1);
}
fseek(f, 0, SEEK_END);
sz = (int)ftell(f);
rewind(f);
pBuf = sqlite3_malloc( sz ? sz : 1 );
if( pBuf==0 ){
fprintf(stderr, "cannot allocate %d to hold content of \"%s\"\n",
sz, zFilename);
exit(1);
}
if( sz>0 ){
if( fread(pBuf, sz, 1, f)!=1 ){
fprintf(stderr, "cannot read all %d bytes of \"%s\"\n", sz, zFilename);
exit(1);
}
fclose(f);
}
*pSz = sz;
*ppBuf = pBuf;
}
/* Array for converting from half-bytes (nybbles) into ASCII hex
** digits. */
static const char hexdigits[] = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
/*
** Render an sqlite3_value as an SQL string.
*/
static void renderValue(sqlite3_value *pVal){
switch( sqlite3_value_type(pVal) ){
case SQLITE_FLOAT: {
double r1;
char zBuf[50];
r1 = sqlite3_value_double(pVal);
sqlite3_snprintf(sizeof(zBuf), zBuf, "%!.15g", r1);
printf("%s", zBuf);
break;
}
case SQLITE_INTEGER: {
printf("%lld", sqlite3_value_int64(pVal));
break;
}
case SQLITE_BLOB: {
char const *zBlob = sqlite3_value_blob(pVal);
int nBlob = sqlite3_value_bytes(pVal);
int i;
printf("x'");
for(i=0; i<nBlob; i++){
putchar(hexdigits[(zBlob[i]>>4)&0x0F]);
putchar(hexdigits[(zBlob[i])&0x0F]);
}
putchar('\'');
break;
}
case SQLITE_TEXT: {
const unsigned char *zArg = sqlite3_value_text(pVal);
putchar('\'');
while( zArg[0] ){
putchar(zArg[0]);
if( zArg[0]=='\'' ) putchar(zArg[0]);
zArg++;
}
putchar('\'');
break;
}
default: {
assert( sqlite3_value_type(pVal)==SQLITE_NULL );
printf("NULL");
break;
}
}
}
/*
** Number of conflicts seen
*/
static int nConflict = 0;
/*
** The conflict callback
*/
static int conflictCallback(
void *pCtx,
int eConflict,
sqlite3_changeset_iter *pIter
){
int op, bIndirect, nCol, i;
const char *zTab;
unsigned char *abPK;
const char *zType = "";
const char *zOp = "";
const char *zSep = " ";
nConflict++;
sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
sqlite3changeset_pk(pIter, &abPK, 0);
switch( eConflict ){
case SQLITE_CHANGESET_DATA: zType = "DATA"; break;
case SQLITE_CHANGESET_NOTFOUND: zType = "NOTFOUND"; break;
case SQLITE_CHANGESET_CONFLICT: zType = "PRIMARY KEY"; break;
case SQLITE_CHANGESET_FOREIGN_KEY: zType = "FOREIGN KEY"; break;
case SQLITE_CHANGESET_CONSTRAINT: zType = "CONSTRAINT"; break;
}
switch( op ){
case SQLITE_UPDATE: zOp = "UPDATE of"; break;
case SQLITE_INSERT: zOp = "INSERT into"; break;
case SQLITE_DELETE: zOp = "DELETE from"; break;
}
printf("%s conflict on %s table %s with primary key", zType, zOp, zTab);
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
if( abPK[i]==0 ) continue;
printf("%s", zSep);
if( op==SQLITE_INSERT ){
sqlite3changeset_new(pIter, i, &pVal);
}else{
sqlite3changeset_old(pIter, i, &pVal);
}
renderValue(pVal);
zSep = ",";
}
printf("\n");
return SQLITE_CHANGESET_OMIT;
}
int main(int argc, char **argv){
int sz, rc;
void *pBuf = 0;
if( argc<3 ) usage(argv[0]);
readFile(argv[1], &sz, &pBuf);
/* changeset FILENAME apply DB
** Apply the changeset in FILENAME to the database file DB
*/
if( strcmp(argv[2],"apply")==0 ){
sqlite3 *db;
if( argc!=4 ) usage(argv[0]);
rc = sqlite3_open(argv[3], &db);
if( rc!=SQLITE_OK ){
fprintf(stderr, "unable to open database file \"%s\": %s\n",
argv[3], sqlite3_errmsg(db));
sqlite3_close(db);
exit(1);
}
sqlite3_exec(db, "BEGIN", 0, 0, 0);
nConflict = 0;
rc = sqlite3changeset_apply(db, sz, pBuf, 0, conflictCallback, 0);
if( rc ){
fprintf(stderr, "sqlite3changeset_apply() returned %d\n", rc);
}
if( nConflict ){
fprintf(stderr, "%d conflicts - no changes applied\n", nConflict);
sqlite3_exec(db, "ROLLBACK", 0, 0, 0);
}else if( rc ){
fprintf(stderr, "sqlite3changeset_apply() returns %d "
"- no changes applied\n", rc);
sqlite3_exec(db, "ROLLBACK", 0, 0, 0);
}else{
sqlite3_exec(db, "COMMIT", 0, 0, 0);
}
sqlite3_close(db);
}else
/* changeset FILENAME concat FILE2 OUT
** Add changeset FILE2 onto the end of the changeset in FILENAME
** and write the result into OUT.
*/
if( strcmp(argv[2],"concat")==0 ){
int szB;
void *pB;
int szOut;
void *pOutBuf;
FILE *out;
const char *zOut = argv[4];
if( argc!=5 ) usage(argv[0]);
out = fopen(zOut, "wb");
if( out==0 ){
fprintf(stderr, "cannot open \"%s\" for writing\n", zOut);
exit(1);
}
readFile(argv[3], &szB, &pB);
sqlite3changeset_concat(sz, pBuf, szB, pB, &szOut, &pOutBuf);
if( fwrite(pOutBuf, szOut, 1, out)!=1 ){
fprintf(stderr, "unable to write all %d bytes of output to \"%s\"\n",
szOut, zOut);
}
fclose(out);
sqlite3_free(pOutBuf);
sqlite3_free(pB);
}else
/* changeset FILENAME dump
** Show the complete content of the changeset in FILENAME
*/
if( strcmp(argv[2],"dump")==0 ){
int cnt = 0;
int i;
sqlite3_changeset_iter *pIter;
rc = sqlite3changeset_start(&pIter, sz, pBuf);
if( rc!=SQLITE_OK ){
fprintf(stderr, "sqlite3changeset_start() returns %d\n", rc);
exit(1);
}
while( sqlite3changeset_next(pIter)==SQLITE_ROW ){
int op, bIndirect, nCol;
const char *zTab;
unsigned char *abPK;
sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
cnt++;
printf("%d: %s table=[%s] indirect=%d nColumn=%d\n",
cnt, op==SQLITE_INSERT ? "INSERT" :
op==SQLITE_UPDATE ? "UPDATE" : "DELETE",
zTab, bIndirect, nCol);
sqlite3changeset_pk(pIter, &abPK, 0);
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
pVal = 0;
sqlite3changeset_old(pIter, i, &pVal);
if( pVal ){
printf(" old[%d]%s = ", i, abPK[i] ? "pk" : " ");
renderValue(pVal);
printf("\n");
}
pVal = 0;
sqlite3changeset_new(pIter, i, &pVal);
if( pVal ){
printf(" new[%d]%s = ", i, abPK[i] ? "pk" : " ");
renderValue(pVal);
printf("\n");
}
}
}
sqlite3changeset_finalize(pIter);
}else
/* changeset FILENAME invert OUT
** Invert the changes in FILENAME and writes the result on OUT
*/
if( strcmp(argv[2],"invert")==0 ){
FILE *out;
int szOut = 0;
void *pOutBuf = 0;
const char *zOut = argv[3];
if( argc!=4 ) usage(argv[0]);
out = fopen(zOut, "wb");
if( out==0 ){
fprintf(stderr, "cannot open \"%s\" for writing\n", zOut);
exit(1);
}
sqlite3changeset_invert(sz, pBuf, &szOut, &pOutBuf);
if( fwrite(pOutBuf, szOut, 1, out)!=1 ){
fprintf(stderr, "unable to write all %d bytes of output to \"%s\"\n",
szOut, zOut);
}
fclose(out);
sqlite3_free(pOutBuf);
}else
/* changeset FILE sql
** Show the content of the changeset as pseudo-SQL
*/
if( strcmp(argv[2],"sql")==0 ){
int cnt = 0;
char *zPrevTab = 0;
char *zSQLTabName = 0;
sqlite3_changeset_iter *pIter = 0;
rc = sqlite3changeset_start(&pIter, sz, pBuf);
if( rc!=SQLITE_OK ){
fprintf(stderr, "sqlite3changeset_start() returns %d\n", rc);
exit(1);
}
printf("BEGIN;\n");
while( sqlite3changeset_next(pIter)==SQLITE_ROW ){
int op, bIndirect, nCol;
const char *zTab;
sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
cnt++;
if( zPrevTab==0 || strcmp(zPrevTab,zTab)!=0 ){
sqlite3_free(zPrevTab);
sqlite3_free(zSQLTabName);
zPrevTab = sqlite3_mprintf("%s", zTab);
if( !isalnum(zTab[0]) || sqlite3_strglob("*[^a-zA-Z0-9]*",zTab)==0 ){
zSQLTabName = sqlite3_mprintf("\"%w\"", zTab);
}else{
zSQLTabName = sqlite3_mprintf("%s", zTab);
}
printf("/****** Changes for table %s ***************/\n", zSQLTabName);
}
switch( op ){
case SQLITE_DELETE: {
unsigned char *abPK;
int i;
const char *zSep = " ";
sqlite3changeset_pk(pIter, &abPK, 0);
printf("/* %d */ DELETE FROM %s WHERE", cnt, zSQLTabName);
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
if( abPK[i]==0 ) continue;
printf("%sc%d=", zSep, i+1);
zSep = " AND ";
sqlite3changeset_old(pIter, i, &pVal);
renderValue(pVal);
}
printf(";\n");
break;
}
case SQLITE_UPDATE: {
unsigned char *abPK;
int i;
const char *zSep = " ";
sqlite3changeset_pk(pIter, &abPK, 0);
printf("/* %d */ UPDATE %s SET", cnt, zSQLTabName);
for(i=0; i<nCol; i++){
sqlite3_value *pVal = 0;
sqlite3changeset_new(pIter, i, &pVal);
if( pVal ){
printf("%sc%d=", zSep, i+1);
zSep = ", ";
renderValue(pVal);
}
}
printf(" WHERE");
zSep = " ";
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
if( abPK[i]==0 ) continue;
printf("%sc%d=", zSep, i+1);
zSep = " AND ";
sqlite3changeset_old(pIter, i, &pVal);
renderValue(pVal);
}
printf(";\n");
break;
}
case SQLITE_INSERT: {
int i;
printf("/* %d */ INSERT INTO %s VALUES", cnt, zSQLTabName);
for(i=0; i<nCol; i++){
sqlite3_value *pVal;
printf("%c", i==0 ? '(' : ',');
sqlite3changeset_new(pIter, i, &pVal);
renderValue(pVal);
}
printf(");\n");
break;
}
}
}
printf("COMMIT;\n");
sqlite3changeset_finalize(pIter);
sqlite3_free(zPrevTab);
sqlite3_free(zSQLTabName);
}else
/* If nothing else matches, show the usage comment */
usage(argv[0]);
sqlite3_free(pBuf);
return 0;
}