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

MerginMaps / geodiff / 26277292931

22 May 2026 08:31AM UTC coverage: 86.567% (-1.5%) from 88.114%
26277292931

Pull #252

github

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

926 of 1098 new or added lines in 13 files covered. (84.34%)

13 existing lines in 3 files now uncovered.

4150 of 4794 relevant lines covered (86.57%)

607.49 hits per line

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

79.35
/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
#include <iostream>
15
#include <unordered_map>
16

17

18
ChangesetTable schemaToChangesetTable( const std::string &tableName, const TableSchema &tbl )
366✔
19
{
20
  ChangesetTable chTable;
366✔
21
  chTable.name = tableName;
366✔
22
  for ( const TableColumnInfo &c : tbl.columns )
1,698✔
23
    chTable.primaryKeys.push_back( c.isPrimaryKey );
1,332✔
24
  return chTable;
366✔
25
}
×
26

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

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

129
    writer.writeEntry( entry );
145✔
130
  }
131
}
57✔
132

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

168

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

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

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

191
  auto entries = nlohmann::json::array();
140✔
192

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

199
    nlohmann::json change;
528✔
200

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

205
      nlohmann::json jsonValueOld = valueToJSON( valueOld );
476✔
206
      nlohmann::json jsonValueNew = valueToJSON( valueNew );
476✔
207

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

223
      entries.push_back( change );
476✔
224
    }
476✔
225
  }
528✔
226

227
  res[ "changes" ] = entries;
140✔
228
  return res;
280✔
229
}
140✔
230

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

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

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

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

310
    entries.push_back( msg );
135✔
311
  }
135✔
312

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

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

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

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

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

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

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

365
nlohmann::json conflictToJSON( const ConflictFeature &conflict )
6✔
366
{
367
  nlohmann::json res;
6✔
368
  res[ "table" ] = std::string( conflict.tableName() );
6✔
369
  res[ "type" ] = "conflict";
6✔
370
  res[ "fid" ] = std::to_string( conflict.pk() );
6✔
371

372
  auto entries = nlohmann::json::array();
6✔
373

374
  const std::vector<ConflictItem> items = conflict.items();
6✔
375
  for ( const ConflictItem &item : items )
13✔
376
  {
377
    nlohmann::json change;
7✔
378
    change[ "column" ] = item.column();
7✔
379

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

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

406
    entries.push_back( change );
7✔
407
  }
7✔
408
  res[ "changes" ] = entries;
6✔
409
  return res;
12✔
410
}
6✔
411

412
nlohmann::json conflictsToJSON( const std::vector<ConflictFeature> &conflicts )
6✔
413
{
414
  auto entries = nlohmann::json::array();
6✔
415
  for ( const ConflictFeature &item : conflicts )
12✔
416
  {
417
    nlohmann::json msg = conflictToJSON( item );
6✔
418
    if ( msg.empty() )
6✔
419
      continue;
×
420

421
    entries.push_back( msg );
6✔
422
  }
6✔
423

424
  nlohmann::json res;
6✔
425
  res[ "geodiff" ] = entries;
6✔
426
  return res;
12✔
427
}
6✔
428

429
inline int hex2num( unsigned char i )
13,854✔
430
{
431
  if ( i <= '9' && i >= '0' )
13,854✔
432
    return i - '0';
10,571✔
433
  if ( i >= 'A' && i <= 'F' )
3,283✔
434
    return 10 + i - 'A';
2✔
435
  if ( i >= 'a' && i <= 'f' )
3,281✔
436
    return 10 + i - 'a';
3,281✔
437
  assert( false );
×
438
  return 0; // should never happen
439
}
440

441
inline char num2hex( int n )
16,724✔
442
{
443
  assert( n >= 0 && n < 16 );
16,724✔
444
  if ( n >= 0 && n < 10 )
16,724✔
445
    return char( '0' + n );
12,753✔
446
  else if ( n >= 10 && n < 16 )
3,971✔
447
    return char( 'A' + n - 10 );
3,971✔
448
  return '?';  // should never happen
×
449
}
450

451
std::string hex2bin( const std::string &str )
69✔
452
{
453
  assert( str.size() % 2 == 0 );
69✔
454
  std::string output( str.size() / 2, 0 );
69✔
455
  for ( size_t i = 0; i < str.size(); i += 2 )
6,996✔
456
  {
457
    int n1 = hex2num( str[i] ), n2 = hex2num( str[i + 1] );
6,927✔
458
    output[i / 2] = char( n1 * 16 + n2 );
6,927✔
459
  }
460
  return output;
69✔
461
}
×
462

463
std::string bin2hex( const std::string &str )
77✔
464
{
465
  std::string output( str.size() * 2, 0 );
77✔
466
  for ( size_t i = 0; i < str.size(); ++i )
8,439✔
467
  {
468
    unsigned char ch = str[i];
8,362✔
469
    output[i * 2] = num2hex( ch / 16 );
8,362✔
470
    output[i * 2 + 1] = num2hex( ch % 16 );
8,362✔
471
  }
472
  return output;
77✔
473
}
×
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