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

MerginMaps / geodiff / 29579363492

17 Jul 2026 12:11PM UTC coverage: 87.239% (-0.4%) from 87.592%
29579363492

Pull #252

github

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

1163 of 1300 new or added lines in 13 files covered. (89.46%)

13 existing lines in 3 files now uncovered.

4348 of 4984 relevant lines covered (87.24%)

652.58 hits per line

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

97.1
/geodiff/src/changesetconcat.cpp
1
/*
2
 GEODIFF - MIT License
3
 Copyright (C) 2021 Martin Dobias
4
*/
5

6
#include "changeset.h"
7

8
#include <optional>
9
#include <string>
10
#include <unordered_map>
11
#include <variant>
12

13
#include "geodifflogger.hpp"
14
#include "geodiffcontext.hpp"
15
#include "geodiffutils.hpp"
16
#include "changesetreader.h"
17
#include "changesetwriter.h"
18

19

20
struct ValueVectorHash
21
{
22
  size_t operator()( const std::vector<Value> &values ) const
378✔
23
  {
24
    size_t h = 0;
378✔
25
    for ( size_t i = 0; i < values.size(); ++i )
756✔
26
      h ^= std::hash<Value> {}( values[i] );
378✔
27
    return h;
378✔
28
  }
29
};
30

31
static std::vector<Value> entryPkey( const ChangesetDataEntry &entry )
455✔
32
{
33
  const std::vector<bool> &pkeys = entry.table->primaryKeys;
455✔
34
  const std::vector<Value> &values = entry.op == ChangesetDataEntry::OpInsert ? entry.newValues : entry.oldValues;
455✔
35
  std::vector<Value> pkeyValues;
455✔
36
  for ( size_t i = 0; i < values.size(); ++i )
1,994✔
37
  {
38
    if ( pkeys[i] )
1,539✔
39
      pkeyValues.push_back( values[i] );
455✔
40
  }
41
  return pkeyValues;
455✔
NEW
42
}
×
43

44

45
// primary key values -> data entry index in entries list
46
typedef std::unordered_map<std::vector<Value>, size_t, ValueVectorHash> TableEntriesMap;
47

48
//! Struct to keep information about table and its changes while concatenating
49
struct TableChanges
50
{
51
  // List of entries affecting this table. Wrapped in optional so we can do
52
  // in-place O(1) deletions.
53
  std::vector<std::optional<ChangesetEntry>> entries;
54
  // Entries output at the start. Used for column additions.
55
  std::vector<ChangesetEntry> prefixEntries;
56
  TableEntriesMap dataEntries;
57
};
58

59
// Output of concatenation is divided into phases, where entries can be freely
60
// merged.
61
// Indexed by table name.
62
typedef std::unordered_map<std::string, TableChanges> OutputPhase;
63

64

65
//! This is a helper function used by mergeUpdate().
66
static Value mergeValue( const Value &vOne, const Value &vTwo )
470✔
67
{
68
  return vTwo.type() != Value::TypeUndefined ? vTwo : vOne;
470✔
69
}
70

71

72
//! This function is used to merge two UPDATE changes on the same row.
73
//! Returns false if the two updates cancel each other and the resulting
74
//! changeset entry can be discarded.
75
static bool mergeUpdate(
67✔
76
  const ChangesetTable &t,
77
  const std::vector<Value> &valuesOld1, const std::vector<Value> &valuesOld2,
78
  const std::vector<Value> &valuesNew1, const std::vector<Value> &valuesNew2,
79
  std::vector<Value> &outputOld, std::vector<Value> &outputNew )
80
{
81
  bool bRequired = false;
67✔
82

83
  for ( size_t i = 0; i < t.columnCount(); ++i )
302✔
84
  {
85
    Value vOld = mergeValue( valuesOld1[i], valuesOld2.size() ? valuesOld2[i] : Value() );
235✔
86
    Value vNew = mergeValue( valuesNew1[i], valuesNew2.size() ? valuesNew2[i] : Value() );
235✔
87

88
    // if there would be no actual changes after the merge, we would discard the merged update...
89
    if ( vOld != vNew && !t.primaryKeys[i] )
235✔
90
      bRequired = true;
94✔
91

92
    // write OLD
93
    if ( t.primaryKeys[i] || vOld != vNew )
235✔
94
    {
95
      outputOld.push_back( vOld );
161✔
96
    }
97
    else
98
    {
99
      outputOld.push_back( Value() );
74✔
100
    }
101

102
    // write NEW
103
    if ( t.primaryKeys[i] || vOld == vNew )
235✔
104
    {
105
      outputNew.push_back( Value() );
141✔
106
    }
107
    else
108
    {
109
      outputNew.push_back( vNew );
94✔
110
    }
111
  }
235✔
112

113
  return bRequired;
67✔
114
}
115

116

117
//! Possible outcomes of merging two changeset entries
118
enum MergeEntriesResult
119
{
120
  EntryModified,   //!< the entry got updated within the merge (INSERT+UPDATE, UPDATE+UPDATE, UPDATE+DELETE, DELETE+INSERT)
121
  EntryRemoved,    //!< the entry should be removed after the merge (INSERT+DELETE)
122
  Unsupported,     //!< unexpected combination (INSERT+INSERT, UPDATE+INSERT, DELETE+UPDATE, DELETE+DELETE)
123
};
124

125
//! Takes two changeset entries e1 and e2 and merges their changes to e1 if possible.
126
//! It is also possible that merging results in no change at all, or the change is not allowed
127
static MergeEntriesResult mergeEntriesForRow( ChangesetDataEntry &e1, const ChangesetDataEntry &e2 )
97✔
128
{
129
  // all these changes make no sense really, if they happen most likely something got broken
130
  // (e.g. adding a row with the same pkey twice)
131
  if ( ( e1.op == ChangesetDataEntry::OpInsert && e2.op == ChangesetDataEntry::OpInsert ) ||
97✔
132
       ( e1.op == ChangesetDataEntry::OpUpdate && e2.op == ChangesetDataEntry::OpInsert ) ||
97✔
133
       ( e1.op == ChangesetDataEntry::OpDelete && e2.op == ChangesetDataEntry::OpUpdate ) ||
97✔
134
       ( e1.op == ChangesetDataEntry::OpDelete && e2.op == ChangesetDataEntry::OpDelete ) )
96✔
135
    return Unsupported;
1✔
136

137
  if ( e1.op == ChangesetDataEntry::OpInsert && e2.op == ChangesetDataEntry::OpDelete )
96✔
138
    return EntryRemoved;
17✔
139

140
  if ( e1.op == ChangesetDataEntry::OpInsert && e2.op == ChangesetDataEntry::OpUpdate )
79✔
141
  {
142
    // modify INSERT - update its values wherever the update has a newer value
143
    for ( size_t i = 0; i < e1.table->columnCount(); ++i )
37✔
144
    {
145
      if ( e2.newValues[i].type() != Value::TypeUndefined )
28✔
146
        e1.newValues[i] = e2.newValues[i];
11✔
147
    }
148
    return EntryModified;
9✔
149
  }
150

151
  if ( e1.op == ChangesetDataEntry::OpUpdate && e2.op == ChangesetDataEntry::OpUpdate )
70✔
152
  {
153
    // modify UPDATE
154
    std::vector<Value> oldVals, newVals;
23✔
155
    if ( !mergeUpdate( *e1.table, e2.oldValues, e1.oldValues, e1.newValues, e2.newValues, oldVals, newVals ) )
23✔
156
      return EntryRemoved;
14✔
157
    e1.oldValues = oldVals;
9✔
158
    e1.newValues = newVals;
9✔
159
    return EntryModified;
9✔
160
  }
23✔
161

162
  if ( e1.op == ChangesetDataEntry::OpUpdate && e2.op == ChangesetDataEntry::OpDelete )
47✔
163
  {
164
    // turn into DELETE, use old values from delete when update does not list them
165
    e1.op = ChangesetDataEntry::OpDelete;
3✔
166
    for ( size_t i = 0; i < e1.table->columnCount(); ++i )
13✔
167
    {
168
      if ( e1.oldValues[i].type() == Value::TypeUndefined )
10✔
169
        e1.oldValues[i] = e2.oldValues[i];
3✔
170
    }
171
    return EntryModified;
3✔
172
  }
173

174
  if ( e1.op == ChangesetDataEntry::OpDelete && e2.op == ChangesetDataEntry::OpInsert )
44✔
175
  {
176
    // turn into UPDATE
177
    std::vector<Value> oldVals, newVals;
44✔
178
    if ( !mergeUpdate( *e1.table, e1.oldValues, {}, e2.newValues, {}, oldVals, newVals ) )
44✔
179
      return EntryRemoved;
5✔
180
    e1.op = ChangesetDataEntry::OpUpdate;
39✔
181
    e1.oldValues = oldVals;
39✔
182
    e1.newValues = newVals;
39✔
183
    return EntryModified;
39✔
184
  }
44✔
185

186
  assert( false ); // all 9 possible cases are exhausted
×
187
  return Unsupported;
188
}
189

190

191
//! Concatenation of multiple changesets, based on the implementation from sqlite3session
192
//! (functions sqlite3changegroup_add() and sqlite3changegroup_output())
193
void concatChangesets(
55✔
194
  const Context *context,
195
  const std::vector<std::string> &filenames,
196
  const std::string &outputChangeset )
197
{
198
  std::vector<OutputPhase> outputPhases = {{}};
165✔
199

200
  for ( const std::string &inputFilename : filenames )
202✔
201
  {
202
    ChangesetReader reader;
147✔
203
    if ( !reader.open( inputFilename ) )
147✔
204
      throw GeoDiffException( "concatChangesets: unable to open input file: " + inputFilename );
×
205

206
    ChangesetEntry fullEntry;
147✔
207
    while ( reader.nextEntry( fullEntry ) )
431✔
208
    {
209
      OutputPhase &phase = outputPhases.back();
284✔
210

211
      if ( ChangesetDataEntry *dEntry = std::get_if<ChangesetDataEntry>( &fullEntry ) )
284✔
212
      {
213
        TableChanges &t = phase[dEntry->table->name];
276✔
214
        auto entriesIt = t.dataEntries.find( entryPkey( *dEntry ) );
276✔
215
        if ( entriesIt == t.dataEntries.end() )
276✔
216
        {
217
          // row with this pkey is not in our list yet
218
          t.entries.push_back( *dEntry );
179✔
219
          t.dataEntries[entryPkey( *dEntry )] = t.entries.size() - 1;
179✔
220
        }
221
        else
222
        {
223
          // we need to merge the recorded entry with the new one
224
          ChangesetDataEntry &entry0 = std::get<ChangesetDataEntry>( *t.entries[entriesIt->second] );
97✔
225
          MergeEntriesResult mergeRes = mergeEntriesForRow( entry0, *dEntry );
97✔
226
          switch ( mergeRes )
97✔
227
          {
228
            case EntryModified:
60✔
229
              break;   // nothing else to do - the original entry got updated in place
60✔
230
            case EntryRemoved:
36✔
231
              t.entries[ entriesIt->second ] = std::nullopt;
36✔
232
              t.dataEntries.erase( entriesIt );
36✔
233
              break;
36✔
234
            case Unsupported:
1✔
235
              // we are discarding the new entry (there's no sensible way to integrate it)
236
              context->logger().warn( "concatChangesets: unsupported sequence of entries for a single row - discarding newer entry" );
2✔
237
              t.entries[ entriesIt->second ] = std::nullopt;
1✔
238
              t.dataEntries.erase( entriesIt );
1✔
239
              break;
1✔
240
          }
241
        }
242
      }
243
      else if ( ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &fullEntry ) )
8✔
244
      {
245
        phase[dtEntry->tableName].entries.push_back( *dtEntry );
1✔
246
      }
247
      else if ( ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &fullEntry ) )
7✔
248
      {
249
        // This entry only contains the column's name, not its index, so we
250
        // can't apply its effects to the existing entries. The best we can do
251
        // is just forward this entry.
252
        phase[dcEntry->tableName].entries.push_back( *dcEntry );
2✔
253
        // We also need to start a new phase, since we can't merge entries
254
        // anymore.
255
        outputPhases.push_back( {{}} );
6✔
256
      }
257
      else if ( ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &fullEntry ) )
5✔
258
      {
259
        phase[ctEntry->tableName].entries = { *ctEntry };
2✔
260
      }
261
      else if ( ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &fullEntry ) )
4✔
262
      {
263
        phase[acEntry->tableName].prefixEntries.push_back( *acEntry );
4✔
264
        // Add the column to all existing entries, since we pushed the column
265
        // addition in front of them.
266
        size_t newColumnCount = SIZE_MAX;
4✔
267
        for ( auto &existingEntry : phase[acEntry->tableName].entries )
5✔
268
        {
269
          if ( !existingEntry ) continue;
1✔
270
          ChangesetDataEntry *existingDEntry = std::get_if<ChangesetDataEntry>( &*existingEntry );
1✔
271
          if ( !existingDEntry ) continue;
1✔
272
          if ( newColumnCount == SIZE_MAX )
1✔
273
            newColumnCount = existingDEntry->table->columnCount() + 1;
1✔
274
          if ( existingDEntry->table->columnCount() != newColumnCount )
1✔
275
            existingDEntry->table->primaryKeys.push_back( false );
1✔
276
          if ( existingDEntry->oldValues.size() != newColumnCount )
1✔
277
            existingDEntry->oldValues.push_back( Value::makeNull() );
1✔
278
          if ( existingDEntry->newValues.size() != newColumnCount )
1✔
279
            existingDEntry->newValues.push_back( Value::makeNull() );
1✔
280
        }
281
      }
282
      else
NEW
283
        throw GeoDiffException( "concatChanges: unhandled entry " + std::to_string( fullEntry.index() ) );
×
284
    }
285
  }
147✔
286

287
  ChangesetWriter writer;
110✔
288
  writer.open( outputChangeset );
55✔
289

290
  // output all we have captured
291
  for ( const OutputPhase &outPhase : outputPhases )
112✔
292
  {
293
    for ( auto it = outPhase.begin(); it != outPhase.end(); ++it )
135✔
294
    {
295
      const TableChanges &t = it->second;
78✔
296

297
      for ( const ChangesetEntry &e : t.prefixEntries )
82✔
298
      {
299
        writer.writeEntry( e );
4✔
300
      }
301

302
      std::shared_ptr<ChangesetTable> writtenSchema;
78✔
303
      for ( const std::optional<ChangesetEntry> &e : t.entries )
261✔
304
      {
305
        if ( e )
183✔
306
        {
307
          if ( const ChangesetDataEntry *dEntry = std::get_if<ChangesetDataEntry>( &*e ) )
146✔
308
          {
309
            if ( dEntry->table != writtenSchema )
142✔
310
            {
311
              writer.beginTable( *dEntry->table );
108✔
312
              writtenSchema = dEntry->table;
108✔
313
            }
314
          }
315
          writer.writeEntry( *e );
146✔
316
        }
317
      }
318
    }
78✔
319
  }
320
}
113✔
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