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

MerginMaps / geodiff / 26279466597

22 May 2026 09:20AM UTC coverage: 86.421% (-1.7%) from 88.114%
26279466597

Pull #252

github

web-flow
Merge db9d774ba into 0a94b5ba4
Pull Request #252: Allow schema changes in diffs

975 of 1160 new or added lines in 13 files covered. (84.05%)

13 existing lines in 3 files now uncovered.

4156 of 4809 relevant lines covered (86.42%)

609.61 hits per line

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

75.94
/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 )
368✔
17
{
18
  ChangesetTable chTable;
368✔
19
  chTable.name = tableName;
368✔
20
  for ( const TableColumnInfo &c : tbl.columns )
1,708✔
21
    chTable.primaryKeys.push_back( c.isPrimaryKey );
1,340✔
22
  return chTable;
368✔
23
}
×
24

25
// Returns inverted changeset entries in reverse order
26
std::vector<ChangesetEntry> invertChangesetReverse( ChangesetReader &reader )
57✔
27
{
28
  std::vector<ChangesetEntry> invertedEntries;
57✔
29
  ChangesetEntry entry;
57✔
30
  while ( reader.nextEntry( entry ) )
202✔
31
  {
32
    if ( ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
145✔
33
    {
34
      if ( dataEntry->op == ChangesetDataEntry::OpInsert )
141✔
35
      {
36
        ChangesetDataEntry out;
86✔
37
        out.op = ChangesetDataEntry::OpDelete;
86✔
38
        out.table = dataEntry->table;
86✔
39
        out.oldValues = dataEntry->newValues;
86✔
40
        invertedEntries.push_back( out );
86✔
41
      }
86✔
42
      else if ( dataEntry->op == ChangesetDataEntry::OpDelete )
55✔
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 )
33✔
51
      {
52
        ChangesetDataEntry out;
33✔
53
        out.op = ChangesetDataEntry::OpUpdate;
33✔
54
        out.table = dataEntry->table;
33✔
55
        out.newValues = dataEntry->oldValues;
33✔
56
        out.oldValues = dataEntry->newValues;
33✔
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 )
152✔
61
        {
62
          if ( dataEntry->table->primaryKeys[i] && out.oldValues[i].type() == Value::TypeUndefined )
119✔
63
          {
64
            out.oldValues[i] = out.newValues[i];
33✔
65
            out.newValues[i].setUndefined();
33✔
66
          }
67
        }
68
        invertedEntries.push_back( out );
33✔
69
      }
33✔
70
      else
71
      {
NEW
72
        throw GeoDiffException( "Unknown entry operation!" );
×
73
      }
74
    }
75
    else if ( const ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &entry ) )
4✔
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 ) )
3✔
83
    {
84
      ChangesetDropColumnEntry out;
1✔
85
      out.tableName = acEntry->tableName;
1✔
86
      out.column = acEntry->column;
1✔
87
      invertedEntries.push_back( out );
1✔
88
    }
1✔
89
    else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
2✔
90
    {
91
      ChangesetCreateTableEntry out;
1✔
92
      out.tableName = dtEntry->tableName;
1✔
93
      out.columns = dtEntry->columns;
1✔
94
      invertedEntries.push_back( out );
1✔
95
    }
1✔
96
    else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
1✔
97
    {
98
      ChangesetAddColumnEntry out;
1✔
99
      out.tableName = dcEntry->tableName;
1✔
100
      out.column = dcEntry->column;
1✔
101
      invertedEntries.push_back( out );
1✔
102
    }
1✔
103
    else
104
    {
NEW
105
      throw GeoDiffException( "Cannot invert changeset entry variant " + std::to_string( entry.index() ) );
×
106
    }
107
  }
108
  return invertedEntries;
114✔
109
}
57✔
110

111
void invertChangeset( ChangesetReader &reader, ChangesetWriter &writer )
57✔
112
{
113
  std::vector<ChangesetEntry> invertedReverse = invertChangesetReverse( reader );
57✔
114
  ChangesetTable *currentTable = nullptr;
57✔
115
  for ( size_t i = 1; i <= invertedReverse.size(); i++ )
202✔
116
  {
117
    const auto &entry = invertedReverse[invertedReverse.size() - i];
145✔
118
    if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
145✔
119
    {
120
      if ( dataEntry->table.get() != currentTable )
141✔
121
      {
122
        writer.beginTable( *dataEntry->table );
72✔
123
        currentTable = dataEntry->table.get();
72✔
124
      }
125
    }
126

127
    writer.writeEntry( entry );
145✔
128
  }
129
}
57✔
130

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

166

167
nlohmann::json changesetDataEntryToJSON( const ChangesetDataEntry &entry )
140✔
168
{
169
  std::string status;
140✔
170
  if ( entry.op == ChangesetDataEntry::OpUpdate )
140✔
171
    status = "update";
31✔
172
  else if ( entry.op == ChangesetDataEntry::OpInsert )
109✔
173
    status = "insert";
95✔
174
  else if ( entry.op == ChangesetDataEntry::OpDelete )
14✔
175
    status = "delete";
14✔
176

177
  // Check that the table column count matches the vector sizes to prevent
178
  // out-of-bounds errors.
179
  if ( ( ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpInsert )
109✔
180
         && entry.table->columnCount() != entry.newValues.size() )
126✔
181
       || ( ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpDelete )
325✔
182
            && entry.table->columnCount() != entry.oldValues.size() ) )
45✔
183
    throw GeoDiffException( "Table column count doesn't match value list size" );
×
184

185
  nlohmann::json res;
140✔
186
  res[ "table" ] = entry.table->name;
140✔
187
  res[ "type" ] = status;
140✔
188

189
  auto entries = nlohmann::json::array();
140✔
190

191
  Value valueOld, valueNew;
140✔
192
  for ( size_t i = 0; i < entry.table->columnCount(); ++i )
668✔
193
  {
194
    valueNew = ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpInsert ) ? entry.newValues[i] : Value();
1,001✔
195
    valueOld = ( entry.op == ChangesetDataEntry::OpUpdate || entry.op == ChangesetDataEntry::OpDelete ) ? entry.oldValues[i] : Value();
707✔
196

197
    nlohmann::json change;
528✔
198

199
    if ( valueNew.type() != Value::TypeUndefined || valueOld.type() != Value::TypeUndefined )
528✔
200
    {
201
      change[ "column" ] = i;
476✔
202

203
      nlohmann::json jsonValueOld = valueToJSON( valueOld );
476✔
204
      nlohmann::json jsonValueNew = valueToJSON( valueNew );
476✔
205

206
      if ( !jsonValueOld.empty() )
476✔
207
      {
208
        if ( jsonValueOld == "null" )
127✔
209
          change[ "old" ] = nullptr;
2✔
210
        else
211
          change[ "old" ] = jsonValueOld;
125✔
212
      }
213
      if ( !jsonValueNew.empty() )
476✔
214
      {
215
        if ( jsonValueNew == "null" )
390✔
216
          change[ "new" ] = nullptr;
61✔
217
        else
218
          change[ "new" ] = jsonValueNew;
329✔
219
      }
220

221
      entries.push_back( change );
476✔
222
    }
476✔
223
  }
528✔
224

225
  res[ "changes" ] = entries;
140✔
226
  return res;
280✔
227
}
140✔
228

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

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

297
nlohmann::json changesetToJSON( ChangesetReader &reader )
61✔
298
{
299
  auto entries = nlohmann::json::array();
61✔
300

301
  ChangesetEntry entry;
61✔
302
  while ( reader.nextEntry( entry ) )
196✔
303
  {
304
    nlohmann::json msg = changesetEntryToJSON( entry );
135✔
305
    if ( msg.empty() )
135✔
306
      continue;
×
307

308
    entries.push_back( msg );
135✔
309
  }
135✔
310

311
  nlohmann::json res;
61✔
312
  res[ "geodiff" ] = entries;
61✔
313
  return res;
122✔
314
}
61✔
315

316
//! auxiliary table used to create table changes summary
317
struct TableSummary
318
{
319
  TableSummary() : inserts( 0 ), updates( 0 ), deletes( 0 ) {}
60✔
320
  int inserts;
321
  int updates;
322
  int deletes;
323
};
324

325
nlohmann::json changesetToJSONSummary( ChangesetReader &reader )
53✔
326
{
327
  std::map< std::string, TableSummary > summary;
53✔
328

329
  ChangesetEntry entry;
53✔
330
  while ( reader.nextEntry( entry ) )
180✔
331
  {
332
    if ( !std::holds_alternative<ChangesetDataEntry>( entry ) )
127✔
NEW
333
      continue;
×
334
    ChangesetDataEntry &dataEntry = std::get<ChangesetDataEntry>( entry );
127✔
335
    std::string tableName = dataEntry.table->name;
127✔
336
    TableSummary &tableSummary = summary[tableName];
127✔
337

338
    if ( dataEntry.op == ChangesetDataEntry::OpUpdate )
127✔
339
      ++tableSummary.updates;
29✔
340
    else if ( dataEntry.op == ChangesetDataEntry::OpInsert )
98✔
341
      ++tableSummary.inserts;
88✔
342
    else if ( dataEntry.op == ChangesetDataEntry::OpDelete )
10✔
343
      ++tableSummary.deletes;
10✔
344
  }
127✔
345

346
  // write JSON
347
  auto entries = nlohmann::json::array();
53✔
348
  for ( const auto &kv : summary )
113✔
349
  {
350
    nlohmann::json tableJson;
60✔
351
    tableJson[ "table" ] = kv.first;
60✔
352
    tableJson[ "insert" ] = kv.second.inserts;
60✔
353
    tableJson[ "update" ] = kv.second.updates;
60✔
354
    tableJson[ "delete" ] = kv.second.deletes;
60✔
355

356
    entries.push_back( tableJson );
60✔
357
  }
60✔
358
  nlohmann::json res;
53✔
359
  res[ "geodiff_summary" ] = entries;
53✔
360
  return res;
106✔
361
}
53✔
362

363
nlohmann::json conflictToJSON( const ConflictFeature &conflict )
6✔
364
{
365
  if ( const DataConflictFeature *c = std::get_if<DataConflictFeature>( &conflict ) )
6✔
366
  {
367
    nlohmann::json res;
6✔
368
    res[ "table" ] = std::string( c->tableName() );
6✔
369
    res[ "type" ] = "conflict";
6✔
370
    res[ "fid" ] = std::to_string( c->pk() );
6✔
371

372
    auto entries = nlohmann::json::array();
6✔
373
    for ( const DataConflictItem &item : c->items() )
13✔
374
    {
375
      nlohmann::json change;
7✔
376
      change[ "column" ] = item.column();
7✔
377

378
      nlohmann::json valueBase = valueToJSON( item.base() );
7✔
379
      nlohmann::json valueOld = valueToJSON( item.theirs() );
7✔
380
      nlohmann::json valueNew = valueToJSON( item.ours() );
7✔
381

382
      if ( !valueBase.empty() )
7✔
383
      {
384
        if ( valueBase == "null" )
7✔
NEW
385
          change[ "base" ] = nullptr;
×
386
        else
387
          change[ "base" ] = valueBase;
7✔
388
      }
389
      if ( !valueOld.empty() )
7✔
390
      {
391
        if ( valueOld == "null" )
7✔
NEW
392
          change[ "old" ] = nullptr;
×
393
        else
394
          change[ "old" ] = valueOld;
7✔
395
      }
396
      if ( !valueNew.empty() )
7✔
397
      {
398
        if ( valueNew == "null" )
7✔
NEW
399
          change[ "new" ] = nullptr;
×
400
        else
401
          change[ "new" ] = valueNew;
7✔
402
      }
403

404
      entries.push_back( change );
7✔
405
    }
7✔
406
    res[ "changes" ] = entries;
6✔
407
    return res;
6✔
408
  }
6✔
NEW
409
  else if ( const TableSchemaConflict *c = std::get_if<TableSchemaConflict>( &conflict ) )
×
410
  {
NEW
411
    nlohmann::json res;
×
NEW
412
    res[ "type" ] = "schema_conflict_table";
×
NEW
413
    res[ "table" ] = c->tableName;
×
NEW
414
    return res;
×
NEW
415
  }
×
NEW
416
  else if ( const ColumnSchemaConflict *c = std::get_if<ColumnSchemaConflict>( &conflict ) )
×
417
  {
NEW
418
    nlohmann::json res;
×
NEW
419
    res[ "type" ] = "schema_conflict_column";
×
NEW
420
    res[ "table" ] = c->tableName;
×
NEW
421
    res[ "column" ] = c->columnName;
×
NEW
422
    return res;
×
NEW
423
  }
×
NEW
424
  return {};
×
425
}
426

427
nlohmann::json conflictsToJSON( const std::vector<ConflictFeature> &conflicts )
6✔
428
{
429
  auto entries = nlohmann::json::array();
6✔
430
  for ( const ConflictFeature &item : conflicts )
12✔
431
    entries.push_back( conflictToJSON( item ) );
6✔
432

433
  nlohmann::json res;
6✔
434
  res[ "geodiff" ] = entries;
6✔
435
  return res;
12✔
436
}
6✔
437

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

450
inline char num2hex( int n )
16,724✔
451
{
452
  assert( n >= 0 && n < 16 );
16,724✔
453
  if ( n >= 0 && n < 10 )
16,724✔
454
    return char( '0' + n );
12,753✔
455
  else if ( n >= 10 && n < 16 )
3,971✔
456
    return char( 'A' + n - 10 );
3,971✔
457
  return '?';  // should never happen
×
458
}
459

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

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

© 2026 Coveralls, Inc