• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MerginMaps / geodiff / 30586949999

30 Jul 2026 10:23PM UTC coverage: 87.091% (-0.5%) from 87.592%
30586949999

Pull #252

github

web-flow
Merge fa6e63982 into e71dfe1a2
Pull Request #252: Allow schema changes in diffs

1317 of 1483 new or added lines in 13 files covered. (88.81%)

15 existing lines in 4 files now uncovered.

4473 of 5136 relevant lines covered (87.09%)

677.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.12
/geodiff/src/changesetutils.cpp
1
/*
2
 GEODIFF - MIT License
3
 Copyright (C) 2020 Martin Dobias
4
*/
5

6
#include "changesetutils.h"
7

8
#include "base64utils.h"
9
#include "changeset.h"
10
#include "geodiffutils.hpp"
11
#include "changesetreader.h"
12
#include "changesetwriter.h"
13
#include "tableschema.h"
14

15

16
ChangesetTable schemaToChangesetTable( const std::string &tableName, const TableSchema &tbl )
419✔
17
{
18
  ChangesetTable chTable;
419✔
19
  chTable.name = tableName;
419✔
20
  for ( const TableColumnInfo &c : tbl.columns )
1,936✔
21
    chTable.primaryKeys.push_back( c.isPrimaryKey );
1,517✔
22
  return chTable;
419✔
23
}
×
24

25
// Returns inverted changeset entries in reverse order
26
std::vector<ChangesetEntry> invertChangesetReverse( ChangesetReader &reader )
62✔
27
{
28
  std::vector<ChangesetEntry> invertedEntries;
62✔
29
  ChangesetEntry entry;
62✔
30
  while ( reader.nextEntry( entry ) )
232✔
31
  {
32
    if ( ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
170✔
33
    {
34
      if ( dataEntry->op == ChangesetDataEntry::OpInsert )
162✔
35
      {
36
        ChangesetDataEntry out;
97✔
37
        out.op = ChangesetDataEntry::OpDelete;
97✔
38
        out.table = dataEntry->table;
97✔
39
        out.oldValues = dataEntry->newValues;
97✔
40
        invertedEntries.push_back( out );
97✔
41
      }
97✔
42
      else if ( dataEntry->op == ChangesetDataEntry::OpDelete )
65✔
43
      {
44
        ChangesetDataEntry out;
22✔
45
        out.op = ChangesetDataEntry::OpInsert;
22✔
46
        out.table = dataEntry->table;
22✔
47
        out.newValues = dataEntry->oldValues;
22✔
48
        invertedEntries.push_back( out );
22✔
49
      }
22✔
50
      else if ( dataEntry->op == ChangesetDataEntry::OpUpdate )
43✔
51
      {
52
        ChangesetDataEntry out;
43✔
53
        out.op = ChangesetDataEntry::OpUpdate;
43✔
54
        out.table = dataEntry->table;
43✔
55
        out.newValues = dataEntry->oldValues;
43✔
56
        out.oldValues = dataEntry->newValues;
43✔
57
        // if a column is a part of pkey and has not been changed,
58
        // the original entry has "old" value the pkey value and "new"
59
        // value is undefined - let's reverse "old" and "new" in that case.
60
        for ( size_t i = 0; i < dataEntry->table->primaryKeys.size(); ++i )
207✔
61
        {
62
          if ( dataEntry->table->primaryKeys[i] && out.oldValues[i].type() == Value::TypeUndefined )
164✔
63
          {
64
            out.oldValues[i] = out.newValues[i];
43✔
65
            out.newValues[i].setUndefined();
43✔
66
          }
67
        }
68
        invertedEntries.push_back( out );
43✔
69
      }
43✔
70
      else
71
      {
NEW
72
        throw GeoDiffException( "Unknown entry operation!" );
×
73
      }
74
    }
75
    else if ( const ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &entry ) )
8✔
76
    {
77
      ChangesetDropTableEntry out;
1✔
78
      out.tableName = ctEntry->tableName;
1✔
79
      out.columns = ctEntry->columns;
1✔
80
      invertedEntries.push_back( out );
1✔
81
    }
1✔
82
    else if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
7✔
83
    {
84
      ChangesetDropColumnEntry out;
2✔
85
      out.tableName = acEntry->tableName;
2✔
86
      // The index of the added column before its deletion is the same as after
87
      // its addition, so it can be carried over as-is.
88
      out.columnIdx = acEntry->columnIdx;
2✔
89
      out.column = acEntry->column;
2✔
90
      invertedEntries.push_back( out );
2✔
91
    }
2✔
92
    else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
5✔
93
    {
94
      ChangesetCreateTableEntry out;
1✔
95
      out.tableName = dtEntry->tableName;
1✔
96
      out.columns = dtEntry->columns;
1✔
97
      invertedEntries.push_back( out );
1✔
98
    }
1✔
99
    else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
4✔
100
    {
101
      ChangesetAddColumnEntry out;
4✔
102
      out.tableName = dcEntry->tableName;
4✔
103
      out.columnIdx = dcEntry->columnIdx;
4✔
104
      out.column = dcEntry->column;
4✔
105
      invertedEntries.push_back( out );
4✔
106
    }
4✔
107
    else
108
    {
NEW
109
      throw GeoDiffException( "Cannot invert changeset entry variant " + std::to_string( entry.index() ) );
×
110
    }
111
  }
112
  return invertedEntries;
124✔
113
}
62✔
114

115
void invertChangeset( ChangesetReader &reader, ChangesetWriter &writer )
62✔
116
{
117
  std::vector<ChangesetEntry> invertedReverse = invertChangesetReverse( reader );
62✔
118
  ChangesetTable *currentTable = nullptr;
62✔
119
  for ( size_t i = 1; i <= invertedReverse.size(); i++ )
232✔
120
  {
121
    const auto &entry = invertedReverse[invertedReverse.size() - i];
170✔
122
    if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
170✔
123
    {
124
      if ( dataEntry->table.get() != currentTable )
162✔
125
      {
126
        writer.beginTable( *dataEntry->table );
81✔
127
        currentTable = dataEntry->table.get();
81✔
128
      }
129
    }
130

131
    writer.writeEntry( entry );
170✔
132
  }
133
}
62✔
134

135
nlohmann::json valueToJSON( const Value &value )
1,015✔
136
{
137
  nlohmann::json j;
1,015✔
138
  switch ( value.type() )
1,015✔
139
  {
140
    case Value::TypeUndefined:
449✔
141
      break;  // actually this not get printed - undefined value should be omitted completely
449✔
142
    case Value::TypeInt:
245✔
143
      j = value.getInt();
245✔
144
      break;
245✔
145
    case Value::TypeDouble:
10✔
146
      j = value.getDouble();
10✔
147
      break;
10✔
148
    case Value::TypeText:
127✔
149
      j = value.getString();
127✔
150
      break;
127✔
151
    case Value::TypeBlob:
115✔
152
    {
153
      // this used to either show "blob N bytes" or would be converted to WKT
154
      // but this is better - it preserves content of any type + can be decoded back
155
      std::string base64 = base64_encode(
156
                             reinterpret_cast<const unsigned char *>( value.getString().data() ),
115✔
157
                             static_cast<unsigned int>( value.getString().size() ) );
230✔
158
      j = base64;
115✔
159
      break;
115✔
160
    }
115✔
161
    case Value::TypeNull:
69✔
162
      j = "null";
69✔
163
      break;
69✔
164
    default:
×
165
      j = "(unknown)";  // should never happen
×
166
  }
167
  return j;
1,015✔
168
}
×
169

170

171
nlohmann::json changesetDataEntryToJSON( const ChangesetDataEntry &entry )
148✔
172
{
173
  std::string status;
148✔
174
  if ( entry.op == ChangesetDataEntry::OpUpdate )
148✔
175
    status = "update";
37✔
176
  else if ( entry.op == ChangesetDataEntry::OpInsert )
111✔
177
    status = "insert";
96✔
178
  else if ( entry.op == ChangesetDataEntry::OpDelete )
15✔
179
    status = "delete";
15✔
180

181
  // Check that the table column count matches the vector sizes to prevent
182
  // out-of-bounds errors.
183
  if ( ( ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpInsert )
111✔
184
         && entry.table->columnCount() != entry.newValues.size() )
133✔
185
       || ( ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpDelete )
348✔
186
            && entry.table->columnCount() != entry.oldValues.size() ) )
52✔
187
    throw GeoDiffException( "Table column count doesn't match value list size" );
×
188

189
  nlohmann::json res;
148✔
190
  res[ "table" ] = entry.table->name;
148✔
191
  res[ "type" ] = status;
148✔
192

193
  auto entries = nlohmann::json::array();
148✔
194

195
  Value valueOld, valueNew;
148✔
196
  for ( size_t i = 0; i < entry.table->columnCount(); ++i )
712✔
197
  {
198
    valueNew = ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpInsert ) ? entry.newValues[i] : Value();
1,070✔
199
    valueOld = ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpDelete ) ? entry.oldValues[i] : Value();
776✔
200

201
    nlohmann::json change;
564✔
202

203
    if ( valueNew.type() != Value::TypeUndefined || valueOld.type() != Value::TypeUndefined )
564✔
204
    {
205
      change[ "column" ] = i;
494✔
206

207
      nlohmann::json jsonValueOld = valueToJSON( valueOld );
494✔
208
      nlohmann::json jsonValueNew = valueToJSON( valueNew );
494✔
209

210
      if ( !jsonValueOld.empty() )
494✔
211
      {
212
        if ( jsonValueOld == "null" )
142✔
213
          change[ "old" ] = nullptr;
5✔
214
        else
215
          change[ "old" ] = jsonValueOld;
137✔
216
      }
217
      if ( !jsonValueNew.empty() )
494✔
218
      {
219
        if ( jsonValueNew == "null" )
399✔
220
          change[ "new" ] = nullptr;
64✔
221
        else
222
          change[ "new" ] = jsonValueNew;
335✔
223
      }
224

225
      entries.push_back( change );
494✔
226
    }
494✔
227
  }
564✔
228

229
  res[ "changes" ] = entries;
148✔
230
  return res;
296✔
231
}
148✔
232

233
static nlohmann::json columnInfoToJSON( const TableColumnInfo &column )
9✔
234
{
235
  nlohmann::json res;
9✔
236
  res["name"] = column.name;
9✔
237
  res["type"] = column.type.dbType;
9✔
238
  res["isPrimaryKey"] = column.isPrimaryKey;
9✔
239
  res["isNotNull"] = column.isNotNull;
9✔
240
  res["isAutoIncrement"] = column.isAutoIncrement;
9✔
241
  res["isGeometry"] = column.isGeometry;
9✔
242
  res["geomType"] = column.geomType;
9✔
243
  res["geomSrsId"] = column.geomSrsId;
9✔
244
  res["geomHasZ"] = column.geomHasZ;
9✔
245
  res["geomHasM"] = column.geomHasM;
9✔
246
  return res;
9✔
NEW
247
}
×
248

249
nlohmann::json changesetEntryToJSON( const ChangesetEntry &entry )
153✔
250
{
251
  if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
153✔
252
  {
253
    return changesetDataEntryToJSON( *dataEntry );
148✔
254
  }
255
  else if ( const ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &entry ) )
5✔
256
  {
257
    nlohmann::json res;
1✔
258
    res["type"] = "create_table";
1✔
259
    res["tableName"] = ctEntry->tableName;
1✔
260
    res["columns"] = nlohmann::json::array();
1✔
261
    for ( const TableColumnInfo &column : ctEntry->columns )
4✔
262
    {
263
      res["columns"].push_back( columnInfoToJSON( column ) );
3✔
264
    }
265
    return res;
1✔
266
  }
1✔
267
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
4✔
268
  {
269
    nlohmann::json res;
1✔
270
    res["type"] = "drop_table";
1✔
271
    res["tableName"] = dtEntry->tableName;
1✔
272
    res["columns"] = nlohmann::json::array();
1✔
273
    for ( const TableColumnInfo &column : dtEntry->columns )
4✔
274
    {
275
      res["columns"].push_back( columnInfoToJSON( column ) );
3✔
276
    }
277
    return res;
1✔
278
  }
1✔
279
  else if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
3✔
280
  {
281
    nlohmann::json res;
2✔
282
    res["type"] = "add_column";
2✔
283
    res["tableName"] = acEntry->tableName;
2✔
284
    res["columnIdx"] = acEntry->columnIdx;
2✔
285
    res["column"] = columnInfoToJSON( acEntry->column );
2✔
286
    return res;
2✔
287
  }
2✔
288
  else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
1✔
289
  {
290
    nlohmann::json res;
1✔
291
    res["type"] = "drop_column";
1✔
292
    res["tableName"] = dcEntry->tableName;
1✔
293
    res["columnIdx"] = dcEntry->columnIdx;
1✔
294
    res["column"] = columnInfoToJSON( dcEntry->column );
1✔
295
    return res;
1✔
296
  }
1✔
297
  else
298
  {
NEW
299
    throw GeoDiffException( "Cannot convert entry variant " + std::to_string( entry.index() ) + " to JSON" );
×
300
  }
301
}
302

303
nlohmann::json changesetToJSON( ChangesetReader &reader )
65✔
304
{
305
  auto entries = nlohmann::json::array();
65✔
306

307
  ChangesetEntry entry;
65✔
308
  while ( reader.nextEntry( entry ) )
212✔
309
  {
310
    nlohmann::json msg = changesetEntryToJSON( entry );
147✔
311
    if ( msg.empty() )
147✔
312
      continue;
×
313

314
    entries.push_back( msg );
147✔
315
  }
147✔
316

317
  nlohmann::json res;
65✔
318
  res[ "geodiff" ] = entries;
65✔
319
  return res;
130✔
320
}
65✔
321

322
//! auxiliary table used to create table changes summary
323
struct TableSummary
324
{
325
  TableSummary() : inserts( 0 ), updates( 0 ), deletes( 0 ) {}
60✔
326
  int inserts;
327
  int updates;
328
  int deletes;
329
};
330

331
nlohmann::json changesetToJSONSummary( ChangesetReader &reader )
53✔
332
{
333
  std::map< std::string, TableSummary > summary;
53✔
334

335
  ChangesetEntry entry;
53✔
336
  while ( reader.nextEntry( entry ) )
180✔
337
  {
338
    if ( !std::holds_alternative<ChangesetDataEntry>( entry ) )
127✔
NEW
339
      continue;
×
340
    ChangesetDataEntry &dataEntry = std::get<ChangesetDataEntry>( entry );
127✔
341
    std::string tableName = dataEntry.table->name;
127✔
342
    TableSummary &tableSummary = summary[tableName];
127✔
343

344
    if ( dataEntry.op == ChangesetDataEntry::OpUpdate )
127✔
345
      ++tableSummary.updates;
29✔
346
    else if ( dataEntry.op == ChangesetDataEntry::OpInsert )
98✔
347
      ++tableSummary.inserts;
88✔
348
    else if ( dataEntry.op == ChangesetDataEntry::OpDelete )
10✔
349
      ++tableSummary.deletes;
10✔
350
  }
127✔
351

352
  // write JSON
353
  auto entries = nlohmann::json::array();
53✔
354
  for ( const auto &kv : summary )
113✔
355
  {
356
    nlohmann::json tableJson;
60✔
357
    tableJson[ "table" ] = kv.first;
60✔
358
    tableJson[ "insert" ] = kv.second.inserts;
60✔
359
    tableJson[ "update" ] = kv.second.updates;
60✔
360
    tableJson[ "delete" ] = kv.second.deletes;
60✔
361

362
    entries.push_back( tableJson );
60✔
363
  }
60✔
364
  nlohmann::json res;
53✔
365
  res[ "geodiff_summary" ] = entries;
53✔
366
  return res;
106✔
367
}
53✔
368

369
nlohmann::json conflictToJSON( const ConflictFeature &conflict )
8✔
370
{
371
  if ( const DataConflictFeature *dcf = std::get_if<DataConflictFeature>( &conflict ) )
8✔
372
  {
373
    nlohmann::json res;
8✔
374
    res[ "table" ] = std::string( dcf->tableName() );
8✔
375
    res[ "type" ] = "conflict";
8✔
376
    res[ "fid" ] = std::to_string( dcf->pk() );
8✔
377

378
    auto entries = nlohmann::json::array();
8✔
379
    for ( const DataConflictItem &item : dcf->items() )
17✔
380
    {
381
      nlohmann::json change;
9✔
382
      change[ "column" ] = item.column();
9✔
383

384
      nlohmann::json valueBase = valueToJSON( item.base() );
9✔
385
      nlohmann::json valueOld = valueToJSON( item.theirs() );
9✔
386
      nlohmann::json valueNew = valueToJSON( item.ours() );
9✔
387

388
      if ( !valueBase.empty() )
9✔
389
      {
390
        if ( valueBase == "null" )
9✔
NEW
391
          change[ "base" ] = nullptr;
×
392
        else
393
          change[ "base" ] = valueBase;
9✔
394
      }
395
      if ( !valueOld.empty() )
9✔
396
      {
397
        if ( valueOld == "null" )
7✔
NEW
398
          change[ "old" ] = nullptr;
×
399
        else
400
          change[ "old" ] = valueOld;
7✔
401
      }
402
      if ( !valueNew.empty() )
9✔
403
      {
404
        if ( valueNew == "null" )
9✔
NEW
405
          change[ "new" ] = nullptr;
×
406
        else
407
          change[ "new" ] = valueNew;
9✔
408
      }
409

410
      entries.push_back( change );
9✔
411
    }
9✔
412
    res[ "changes" ] = entries;
8✔
413
    return res;
8✔
414
  }
8✔
NEW
415
  else if ( const TableSchemaConflict *tsc = std::get_if<TableSchemaConflict>( &conflict ) )
×
416
  {
NEW
417
    nlohmann::json res;
×
NEW
418
    res[ "type" ] = "schema_conflict_table";
×
NEW
419
    res[ "table" ] = tsc->tableName;
×
NEW
420
    return res;
×
NEW
421
  }
×
NEW
422
  else if ( const ColumnSchemaConflict *csc = std::get_if<ColumnSchemaConflict>( &conflict ) )
×
423
  {
NEW
424
    nlohmann::json res;
×
NEW
425
    res[ "type" ] = "schema_conflict_column";
×
NEW
426
    res[ "table" ] = csc->tableName;
×
NEW
427
    res[ "column" ] = csc->columnName;
×
NEW
428
    return res;
×
NEW
429
  }
×
NEW
430
  return {};
×
431
}
432

433
nlohmann::json conflictsToJSON( const std::vector<ConflictFeature> &conflicts )
8✔
434
{
435
  auto entries = nlohmann::json::array();
8✔
436
  for ( const ConflictFeature &item : conflicts )
16✔
437
    entries.push_back( conflictToJSON( item ) );
8✔
438

439
  nlohmann::json res;
8✔
440
  res[ "geodiff" ] = entries;
8✔
441
  return res;
16✔
442
}
8✔
443

444
inline int hex2num( unsigned char i )
13,854✔
445
{
446
  if ( i <= '9' && i >= '0' )
13,854✔
447
    return i - '0';
10,571✔
448
  if ( i >= 'A' && i <= 'F' )
3,283✔
449
    return 10 + i - 'A';
2✔
450
  if ( i >= 'a' && i <= 'f' )
3,281✔
451
    return 10 + i - 'a';
3,281✔
452
  assert( false );
×
453
  return 0; // should never happen
454
}
455

456
inline char num2hex( int n )
16,724✔
457
{
458
  assert( n >= 0 && n < 16 );
16,724✔
459
  if ( n >= 0 && n < 10 )
16,724✔
460
    return char( '0' + n );
12,753✔
461
  else if ( n >= 10 && n < 16 )
3,971✔
462
    return char( 'A' + n - 10 );
3,971✔
463
  return '?';  // should never happen
×
464
}
465

466
std::string hex2bin( const std::string &str )
69✔
467
{
468
  assert( str.size() % 2 == 0 );
69✔
469
  std::string output( str.size() / 2, 0 );
69✔
470
  for ( size_t i = 0; i < str.size(); i += 2 )
6,996✔
471
  {
472
    int n1 = hex2num( str[i] ), n2 = hex2num( str[i + 1] );
6,927✔
473
    output[i / 2] = char( n1 * 16 + n2 );
6,927✔
474
  }
475
  return output;
69✔
476
}
×
477

478
std::string bin2hex( const std::string &str )
77✔
479
{
480
  std::string output( str.size() * 2, 0 );
77✔
481
  for ( size_t i = 0; i < str.size(); ++i )
8,439✔
482
  {
483
    unsigned char ch = str[i];
8,362✔
484
    output[i * 2] = num2hex( ch / 16 );
8,362✔
485
    output[i * 2 + 1] = num2hex( ch % 16 );
8,362✔
486
  }
487
  return output;
77✔
488
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc