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

MerginMaps / geodiff / 20332653776

18 Dec 2025 09:42AM UTC coverage: 88.298% (-1.7%) from 90.035%
20332653776

push

github

wonder-sk
Fix updating rows with microsecond timestamps in Postgres

1 of 1 new or added line in 1 file covered. (100.0%)

237 existing lines in 6 files now uncovered.

3584 of 4059 relevant lines covered (88.3%)

582.99 hits per line

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

90.89
/geodiff/src/geodiff-cli.cpp
1
/*
2
 GEODIFF - MIT License
3
 Copyright (C) 2019 Peter Petrik
4
*/
5

6
#include "geodiff.h"
7
#include <iostream>
8
#include <stdio.h>
9
#include <string.h>
10
#include <string>
11

12
#include <fstream>
13
#include <vector>
14
#include <sstream>
15
#include "geodiffutils.hpp"
16
#include "geodiffcontext.hpp"
17
#include "driver.h"
18

19
static bool fileToStdout( const std::string &filepath )
12✔
20
{
21
#ifdef WIN32
22
  std::ifstream f;
23
  try
24
  {
25
    f.open( stringToWString( filepath ) );
26
  }
27
  catch ( GeoDiffException & )
28
  {
29
    return false;
30
  }
31
#else
32
  std::ifstream f( filepath );
12✔
33
#endif
34
  if ( !f.is_open() )
12✔
35
    return false;
×
36

37
  std::cout << f.rdbuf();
12✔
38
  return true;
12✔
39
}
12✔
40

41
static bool parseRequiredArgument( std::string &value, const std::vector<std::string> &args, size_t &i, const std::string &argName, const std::string &cmdName )
151✔
42
{
43
  if ( i < args.size() )
151✔
44
  {
45
    value = args[i++];
124✔
46
    return true;
124✔
47
  }
48
  else
49
  {
50
    std::cout << "Error: missing " << argName << " for '" << cmdName << "' command." << std::endl;
27✔
51
    return false;
27✔
52
  }
53
}
54

55
static bool checkNoExtraArguments( const std::vector<std::string> &args, size_t i, const std::string &cmdName )
42✔
56
{
57
  if ( i < args.size() )
42✔
58
  {
59
    std::cout << "Error: unexpected extra arguments for '" << cmdName << "' command." << std::endl;
12✔
60
    return false;
12✔
61
  }
62
  return true;
30✔
63
}
64

65
static bool isOption( const std::string &str )
86✔
66
{
67
  return str.size() > 0 && str[0] == '-';
86✔
68
}
69

70
static bool parseDriverOption( const std::vector<std::string> &args, size_t &i, const std::string &cmdName, std::string &driverName, std::string &driverOptions, std::string &tablesToSkip )
43✔
71
{
72
  for ( ; i < args.size(); ++i )
48✔
73
  {
74
    if ( !isOption( args[i] ) )
42✔
75
      break;  // no more options
27✔
76

77
    if ( args[i] == "--driver" )
15✔
78
    {
79
      if ( i + 2 >= args.size() )
10✔
80
      {
81
        std::cout << "Error: missing arguments for driver option" << std::endl;
7✔
82
        return false;
7✔
83
      }
84
      driverName = args[i + 1];
3✔
85
      driverOptions = args[i + 2];
3✔
86
      i += 2;
3✔
87
      continue;
3✔
88
    }
89
    else if ( args[i] == "--skip-tables" )
5✔
90
    {
91
      if ( i + 1 >= args.size() )
3✔
92
      {
93
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
1✔
94
        return 1;
1✔
95
      }
96
      tablesToSkip = args[i + 1];
2✔
97
      i += 1;
2✔
98
      continue;
2✔
99
    }
100
    else
101
    {
102
      std::cout << "Error: unknown option '" << args[i] << "' for '" << cmdName << "' command." << std::endl;
2✔
103
      return false;
2✔
104
    }
105
  }
106
  return true;
33✔
107
}
108

109
static std::vector<std::string> parseIgnoredTables( std::string &tablesToSkip )
33✔
110
{
111
  std::vector<std::string> tables;
33✔
112

113
  std::istringstream strm( tablesToSkip );
33✔
114
  std::string s;
33✔
115
  while ( getline( strm, s, ';' ) )
37✔
116
  {
117
    tables.push_back( s );
4✔
118
  }
119

120
  return tables;
66✔
121
}
33✔
122

123

124
static int handleCmdDiff( GEODIFF_ContextH context, const std::vector<std::string> &args )
18✔
125
{
126
  //   geodiff diff [OPTIONS...] DB_1 DB_2 [CH_OUTPUT]
127

128
  bool writeJson = false;
18✔
129
  bool writeSummary = false;
18✔
130
  bool printOutput = true;
18✔
131
  std::string db1, db2, chOutput;
18✔
132
  std::string driver1Name = "sqlite", driver2Name = "sqlite", driver1Options, driver2Options;
72✔
133
  std::string tablesToSkip;
18✔
134
  size_t i = 1;
18✔
135

136
  // parse options
137
  for ( ; i < args.size(); ++i )
31✔
138
  {
139
    if ( !isOption( args[i] ) )
30✔
140
      break;  // no more options
12✔
141

142
    if ( args[i] == "--json" )
18✔
143
    {
144
      writeJson = true;
4✔
145
      continue;
4✔
146
    }
147
    else if ( args[i] == "--summary" )
14✔
148
    {
149
      writeSummary = true;
4✔
150
      continue;
4✔
151
    }
152
    else if ( args[i] == "--driver" || args[i] == "--driver-1" || args[i] == "--driver-2" )
10✔
153
    {
154
      if ( i + 2 >= args.size() )
3✔
155
      {
156
        std::cout << "Error: missing arguments for driver option" << std::endl;
×
157
        return 1;
×
158
      }
159
      if ( args[i] == "--driver" || args[i] == "--driver-1" )
3✔
160
      {
161
        driver1Name = args[i + 1];
2✔
162
        driver1Options = args[i + 2];
2✔
163
      }
164
      if ( args[i] == "--driver" || args[i] == "--driver-2" )
3✔
165
      {
166
        driver2Name = args[i + 1];
2✔
167
        driver2Options = args[i + 2];
2✔
168
      }
169
      i += 2;
3✔
170
      continue;
3✔
171
    }
172
    else if ( args[i] == "--skip-tables" )
7✔
173
    {
174
      if ( i + 1 >= args.size() )
3✔
175
      {
176
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
1✔
177
        return 1;
1✔
178
      }
179
      tablesToSkip = args[i + 1];
2✔
180
      i += 1;
2✔
181
      continue;
2✔
182
    }
183
    else
184
    {
185
      std::cout << "Error: unknown option '" << args[i] << "' for 'diff' command." << std::endl;
4✔
186
      return 1;
4✔
187
    }
188
  }
189

190
  // check validity of options
191
  if ( writeJson && writeSummary )
13✔
192
  {
193
    std::cout << "Error: only one of the options can be passed: --json or --summary" << std::endl;
1✔
194
    return 1;
1✔
195
  }
196

197
  // set tables to skip
198
  Context *ctx = static_cast<Context *>( context );
12✔
199
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
12✔
200
  ctx->setTablesToSkip( tables );
12✔
201

202
  // parse required arguments
203
  if ( !parseRequiredArgument( db1, args, i, "DB_1", "diff" ) )
48✔
204
    return 1;
1✔
205
  if ( !parseRequiredArgument( db2, args, i, "DB_2", "diff" ) )
44✔
206
    return 1;
2✔
207

208
  if ( i < args.size() )
9✔
209
  {
210
    // optional output argument
211
    printOutput = false;
2✔
212
    chOutput = args[i++];
2✔
213

214
    if ( !checkNoExtraArguments( args, i, "diff" ) )
4✔
215
      return 1;
1✔
216
  }
217

218
  std::string changeset;
8✔
219
  TmpFile tmpChangeset;
8✔
220
  if ( printOutput || writeJson || writeSummary )
8✔
221
  {
222
    changeset = randomTmpFilename( );
7✔
223
    tmpChangeset.setPath( changeset );
7✔
224
  }
225
  else
226
    changeset = chOutput;
1✔
227

228
  if ( driver1Name == driver2Name && driver1Options == driver2Options )
8✔
229
  {
230
    int ret = GEODIFF_createChangesetEx( context,
8✔
231
                                         driver1Name.data(), driver1Options.data(),
8✔
232
                                         db1.data(), db2.data(), changeset.data() );
8✔
233
    if ( ret != GEODIFF_SUCCESS )
8✔
234
    {
235
      std::cout << "Error: diff failed!" << std::endl;
1✔
236
      return 1;
1✔
237
    }
238
  }
239
  else
240
  {
241
    int ret = GEODIFF_createChangesetDr( context,
×
UNCOV
242
                                         driver1Name.data(), driver1Options.data(), db1.data(),
×
UNCOV
243
                                         driver2Name.data(), driver2Options.data(), db2.data(),
×
UNCOV
244
                                         changeset.data() );
×
245
    if ( ret != GEODIFF_SUCCESS )
×
246
    {
247
      std::cout << "Error: diff failed!" << std::endl;
×
248
      return 1;
×
249
    }
250
  }
251

252
  if ( writeJson || writeSummary )
7✔
253
  {
254
    std::string json;
5✔
255
    TmpFile tmpJson;
5✔
256
    if ( printOutput )
5✔
257
    {
258
      json = randomTmpFilename( );
5✔
259
      tmpJson.setPath( json );
5✔
260
    }
261
    else
262
      json = chOutput;
×
263

264
    if ( writeJson )
5✔
265
    {
266
      if ( GEODIFF_listChanges( context, changeset.data(), json.data() ) != GEODIFF_SUCCESS )
2✔
267
      {
268
        std::cout << "Error: failed to convert changeset to JSON!" << std::endl;
×
269
        return 1;
×
270
      }
271
    }
272
    else  // writeSummary
273
    {
274
      if ( GEODIFF_listChangesSummary( context, changeset.data(), json.data() ) != GEODIFF_SUCCESS )
3✔
275
      {
276
        std::cout << "Error: failed to convert changeset to summary!" << std::endl;
×
277
        return 1;
×
278
      }
279
    }
280

281
    if ( printOutput )
5✔
282
    {
283
      if ( !fileToStdout( json ) )
5✔
284
      {
285
        std::cout << "Error: unable to read content of file " << json << std::endl;
×
286
        return 1;
×
287
      }
288
    }
289
  }
10✔
290
  else if ( printOutput )
2✔
291
  {
292
    if ( !fileToStdout( changeset ) )
1✔
293
    {
294
      std::cout << "Error: unable to read content of file " << changeset << std::endl;
×
295
      return 1;
×
296
    }
297
  }
298

299
  return 0;
7✔
300
}
18✔
301

302

303
static int handleCmdApply( GEODIFF_ContextH context, const std::vector<std::string> &args )
13✔
304
{
305
  // geodiff apply [OPTIONS...] DB CH_INPUT
306

307
  size_t i = 1;
13✔
308
  std::string db, changeset;
13✔
309
  std::string driverName = "sqlite", driverOptions;
26✔
310
  std::string tablesToSkip;
13✔
311

312
  // parse options
313
  if ( !parseDriverOption( args, i, "apply", driverName, driverOptions, tablesToSkip ) )
26✔
314
    return 1;
2✔
315

316
  // parse required arguments
317
  if ( !parseRequiredArgument( db, args, i, "DB", "apply" ) )
44✔
318
    return 1;
2✔
319
  if ( !parseRequiredArgument( changeset, args, i, "CH_INPUT", "apply" ) )
36✔
320
    return 1;
4✔
321
  if ( !checkNoExtraArguments( args, i, "apply" ) )
10✔
322
    return 1;
2✔
323

324
  Context *ctx = static_cast<Context *>( context );
3✔
325
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
3✔
326
  ctx->setTablesToSkip( tables );
3✔
327

328
  int ret = GEODIFF_applyChangesetEx( context, driverName.data(), driverOptions.data(), db.data(), changeset.data() );
3✔
329
  if ( ret != GEODIFF_SUCCESS )
3✔
330
  {
331
    std::cout << "Error: apply changeset failed!" << std::endl;
1✔
332
    return 1;
1✔
333
  }
334

335
  return 0;
2✔
336
}
13✔
337

338
static int handleCmdRebaseDiff( GEODIFF_ContextH context,  const std::vector<std::string> &args )
10✔
339
{
340
  // geodiff rebase-diff [OPTIONS...] DB_BASE CH_BASE_OUR CH_BASE_THEIR CH_REBASED CONFLICT
341

342
  size_t i = 1;
10✔
343
  std::string dbBase, chBaseOur, chBaseTheir, chRebased, conflict;
10✔
344
  std::string driverName = "sqlite", driverOptions;
20✔
345
  std::string tablesToSkip;
10✔
346

347
  // parse options
348
  if ( !parseDriverOption( args, i, "rebase-diff", driverName, driverOptions, tablesToSkip ) )
20✔
349
    return 1;
2✔
350

351
  if ( !parseRequiredArgument( dbBase, args, i, "DB_BASE", "rebase-diff" ) )
32✔
352
    return 1;
1✔
353
  if ( !parseRequiredArgument( chBaseOur, args, i, "CH_BASE_OUR", "rebase-diff" ) )
28✔
354
    return 1;
1✔
355
  if ( !parseRequiredArgument( chBaseTheir, args, i, "CH_BASE_THEIR", "rebase-diff" ) )
24✔
356
    return 1;
1✔
357
  if ( !parseRequiredArgument( chRebased, args, i, "CH_REBASED", "rebase-diff" ) )
20✔
358
    return 1;
×
359
  if ( !parseRequiredArgument( conflict, args, i, "CONFLICT", "rebase-diff" ) )
20✔
360
    return 1;
1✔
361
  if ( !checkNoExtraArguments( args, i, "rebase-diff" ) )
8✔
362
    return 1;
1✔
363

364
  Context *ctx = static_cast<Context *>( context );
3✔
365
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
3✔
366
  ctx->setTablesToSkip( tables );
3✔
367

368
  int ret = GEODIFF_createRebasedChangesetEx(
3✔
369
              context,
370
              driverName.data(), driverOptions.data(),
3✔
371
              dbBase.data(), chBaseOur.data(),
3✔
372
              chBaseTheir.data(), chRebased.data(), conflict.data() );
3✔
373
  if ( ret != GEODIFF_SUCCESS )
3✔
374
  {
375
    std::cout << "Error: rebase-diff failed!" << std::endl;
2✔
376
    return 1;
2✔
377
  }
378

379
  return 0;
1✔
380
}
10✔
381

382
static int handleCmdRebaseDb( GEODIFF_ContextH context, const std::vector<std::string> &args )
9✔
383
{
384
  // geodiff rebase-db [OPTIONS...] DB_BASE DB_OUR CH_BASE_THEIR CONFLICT
385

386
  size_t i = 1;
9✔
387
  std::string dbBase, dbOur, chBaseTheir, conflict;
9✔
388
  std::string driverName = "sqlite", driverOptions;
18✔
389
  std::string tablesToSkip;
9✔
390

391
  // parse options
392
  if ( !parseDriverOption( args, i, "rebase-db", driverName, driverOptions, tablesToSkip ) )
18✔
393
    return 1;
3✔
394

395
  if ( !parseRequiredArgument( dbBase, args, i, "DB_BASE", "rebase-db" ) )
24✔
396
    return 1;
1✔
397
  if ( !parseRequiredArgument( dbOur, args, i, "DB_OUR", "rebase-db" ) )
20✔
398
    return 1;
×
399
  if ( !parseRequiredArgument( chBaseTheir, args, i, "CH_BASE_THEIR", "rebase-db" ) )
20✔
400
    return 1;
×
401
  if ( !parseRequiredArgument( conflict, args, i, "CONFLICT", "rebase-db" ) )
20✔
402
    return 1;
1✔
403
  if ( !checkNoExtraArguments( args, i, "rebase-db" ) )
8✔
404
    return 1;
×
405

406
  // set tables to skip
407
  Context *ctx = static_cast<Context *>( context );
4✔
408
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
4✔
409
  ctx->setTablesToSkip( tables );
4✔
410

411
  int ret = GEODIFF_rebaseEx( context,
4✔
412
                              driverName.data(), driverOptions.data(), dbBase.data(), dbOur.data(),
4✔
413
                              chBaseTheir.data(), conflict.data() );
4✔
414
  if ( ret != GEODIFF_SUCCESS )
4✔
415
  {
416
    std::cout << "Error: rebase-db failed!" << std::endl;
3✔
417
    return 1;
3✔
418
  }
419

420
  return 0;
1✔
421
}
9✔
422

423
static int handleCmdInvert( GEODIFF_ContextH context, const std::vector<std::string> &args )
6✔
424
{
425
  // geodiff invert CH_INPUT CH_OUTPUT
426

427
  size_t i = 1;
6✔
428
  std::string chInput, chOutput;
6✔
429

430
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "invert" ) )
24✔
431
    return 1;
1✔
432
  if ( !parseRequiredArgument( chOutput, args, i, "CH_OUTPUT", "invert" ) )
20✔
433
    return 1;
2✔
434
  if ( !checkNoExtraArguments( args, i, "invert" ) )
6✔
435
    return 1;
1✔
436

437
  int ret = GEODIFF_invertChangeset( context, chInput.data(), chOutput.data() );
2✔
438
  if ( ret != GEODIFF_SUCCESS )
2✔
439
  {
440
    std::cout << "Error: invert changeset failed!" << std::endl;
1✔
441
    return 1;
1✔
442
  }
443

444
  return 0;
1✔
445
}
6✔
446

447
static int handleCmdConcat( GEODIFF_ContextH context, const std::vector<std::string> &args )
4✔
448
{
449
  // geodiff concat CH_INPUT_1 CH_INPUT_2 [...] CH_OUTPUT
450

451
  if ( args.size() < 4 )
4✔
452
  {
453
    std::cout << "Error: 'concat' command needs at least two input changesets and one output changeset." << std::endl;
1✔
454
    return 1;
1✔
455
  }
456

457
  std::string chOutput = args[args.size() - 1];
3✔
458

459
  std::vector<const char *> changesets;
3✔
460
  for ( size_t i = 1; i < args.size() - 1; ++i )
10✔
461
  {
462
    changesets.push_back( args[i].data() );
7✔
463
  }
464

465
  int ret = GEODIFF_concatChanges( context, ( int ) changesets.size(), changesets.data(), chOutput.data() );
3✔
466
  if ( ret != GEODIFF_SUCCESS )
3✔
467
  {
468
    std::cout << "Error: concat changesets failed!" << std::endl;
1✔
469
    return 1;
1✔
470
  }
471

472
  return 0;
2✔
473
}
3✔
474

475
static int handleCmdAsJson( GEODIFF_ContextH context, const std::vector<std::string> &args )
8✔
476
{
477
  // geodiff as-json CH_INPUT [CH_OUTPUT]
478

479
  size_t i = 1;
8✔
480
  bool printOutput = true;
8✔
481
  std::string chInput, chOutput;
8✔
482

483
  // parse required arguments
484
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "as-json" ) )
32✔
485
    return 1;
1✔
486

487
  if ( i < args.size() )
7✔
488
  {
489
    // optional output argument
490
    printOutput = false;
3✔
491
    chOutput = args[i++];
3✔
492

493
    if ( !checkNoExtraArguments( args, i, "as-json" ) )
6✔
494
      return 1;
1✔
495
  }
496

497
  std::string changeset;
6✔
498
  TmpFile tmpChangeset;
6✔
499
  if ( printOutput )
6✔
500
  {
501
    changeset = randomTmpFilename( );
4✔
502
    tmpChangeset.setPath( changeset );
4✔
503
  }
504
  else
505
    changeset = chOutput;
2✔
506

507
  int ret = GEODIFF_listChanges( context, chInput.data(), changeset.data() );
6✔
508
  if ( ret != GEODIFF_SUCCESS )
6✔
509
  {
510
    std::cout << "Error: export changeset to JSON failed!" << std::endl;
1✔
511
    return 1;
1✔
512
  }
513

514
  if ( printOutput )
5✔
515
  {
516
    if ( !fileToStdout( changeset ) )
4✔
517
    {
518
      std::cout << "Error: unable to read content of file " << changeset << std::endl;
×
519
      return 1;
×
520
    }
521
  }
522

523
  return 0;
5✔
524
}
8✔
525

526
static int handleCmdAsSummary( GEODIFF_ContextH context, const std::vector<std::string> &args )
4✔
527
{
528
  // geodiff as-summary CH_INPUT [SUMMARY]
529

530
  size_t i = 1;
4✔
531
  bool printOutput = true;
4✔
532
  std::string chInput, chOutput;
4✔
533

534
  // parse required arguments
535
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "as-summary" ) )
16✔
536
    return 1;
1✔
537

538
  if ( i < args.size() )
3✔
539
  {
540
    // optional output argument
541
    printOutput = false;
2✔
542
    chOutput = args[i++];
2✔
543

544
    if ( !checkNoExtraArguments( args, i, "as-summary" ) )
4✔
545
      return 1;
×
546
  }
547

548
  std::string summary;
3✔
549
  TmpFile tmpSummary;
3✔
550
  if ( printOutput )
3✔
551
  {
552
    summary = randomTmpFilename( );
1✔
553
    tmpSummary.setPath( summary );
1✔
554
  }
555
  else
556
    summary = chOutput;
2✔
557

558
  int ret = GEODIFF_listChangesSummary( context, chInput.data(), summary.data() );
3✔
559
  if ( ret != GEODIFF_SUCCESS )
3✔
560
  {
561
    std::cout << "Error: export changeset to summary failed!" << std::endl;
1✔
562
    return 1;
1✔
563
  }
564

565
  if ( printOutput )
2✔
566
  {
567
    if ( !fileToStdout( summary ) )
1✔
568
    {
569
      std::cout << "Error: unable to read content of file " << summary << std::endl;
×
570
      return 1;
×
571
    }
572
  }
573

574
  return 0;
2✔
575
}
4✔
576

577
static int handleCmdCopy( GEODIFF_ContextH context, const std::vector<std::string> &args )
12✔
578
{
579
  // geodiff copy [OPTIONS...] DB_SOURCE DB_DESTINATION
580

581
  size_t i = 1;
12✔
582
  std::string chInput, chOutput;
12✔
583
  std::string driver1Name = "sqlite", driver1Options, driver2Name = "sqlite", driver2Options;
48✔
584
  std::string tablesToSkip;
12✔
585

586
  // parse options
587
  for ( ; i < args.size(); ++i )
17✔
588
  {
589
    if ( !isOption( args[i] ) )
14✔
590
      break;  // no more options
9✔
591

592
    if ( args[i] == "--driver" || args[i] == "--driver-1" || args[i] == "--driver-2" )
5✔
593
    {
594
      if ( i + 2 >= args.size() )
3✔
595
      {
596
        std::cout << "Error: missing arguments for driver option" << std::endl;
×
597
        return 1;
×
598
      }
599
      if ( args[i] == "--driver" || args[i] == "--driver-1" )
3✔
600
      {
601
        driver1Name = args[i + 1];
2✔
602
        driver1Options = args[i + 2];
2✔
603
      }
604
      if ( args[i] == "--driver" || args[i] == "--driver-2" )
3✔
605
      {
606
        driver2Name = args[i + 1];
2✔
607
        driver2Options = args[i + 2];
2✔
608
      }
609
      i += 2;
3✔
610
      continue;
3✔
611
    }
612
    else if ( args[i] == "--skip-tables" )
2✔
613
    {
614
      if ( i + 1 >= args.size() )
2✔
615
      {
616
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
×
617
        return 1;
×
618
      }
619
      tablesToSkip = args[i + 1];
2✔
620
      i += 1;
2✔
621
    }
622
    else
623
    {
624
      std::cout << "Error: unknown option '" << args[i] << "' for 'copy' command." << std::endl;
×
625
      return 1;
×
626
    }
627
  }
628

629
  if ( !parseRequiredArgument( chInput, args, i, "DB_SOURCE", "copy" ) )
48✔
630
    return 1;
3✔
631
  if ( !parseRequiredArgument( chOutput, args, i, "DB_DESTINATION", "copy" ) )
36✔
632
    return 1;
1✔
633
  if ( !checkNoExtraArguments( args, i, "copy" ) )
16✔
634
    return 1;
1✔
635

636
  Context *ctx = static_cast<Context *>( context );
7✔
637
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
7✔
638
  ctx->setTablesToSkip( tables );
7✔
639

640
  if ( driver1Name == "sqlite" && driver2Name == "sqlite" )
7✔
641
  {
642
    if ( !tablesToSkip.empty() )
7✔
643
    {
644
      std::cout << "Source and destination drivers are \"sqlite\". Option \"--skip-tables\" will be ignored." << std::endl;
1✔
645
      return 1;
1✔
646
    }
647

648
    int ret = GEODIFF_makeCopySqlite( context, chInput.data(), chOutput.data() );
6✔
649
    if ( ret != GEODIFF_SUCCESS )
6✔
650
    {
651
      std::cout << "Error: copy failed!" << std::endl;
1✔
652
      return 1;
1✔
653
    }
654
  }
655
  else
656
  {
657
    int ret = GEODIFF_makeCopy( context,
×
UNCOV
658
                                driver1Name.data(), driver1Options.data(), chInput.data(),
×
UNCOV
659
                                driver2Name.data(), driver2Options.data(), chOutput.data() );
×
660
    if ( ret != GEODIFF_SUCCESS )
×
661
    {
662
      std::cout << "Error: copy failed!" << std::endl;
×
663
      return 1;
×
664
    }
665
  }
666

667
  return 0;
5✔
668
}
12✔
669

670
static int handleCmdSchema( GEODIFF_ContextH context, const std::vector<std::string> &args )
5✔
671
{
672
  // geodiff schema [OPTIONS...] DB [SCHEMA_JSON]
673

674
  size_t i = 1;
5✔
675
  bool printOutput = true;
5✔
676
  std::string db, schemaJson;
5✔
677
  std::string driverName = "sqlite", driverOptions;
10✔
678
  std::string tablesToSkip;
5✔
679

680
  // parse options
681
  if ( !parseDriverOption( args, i, "schema", driverName, driverOptions, tablesToSkip ) )
10✔
682
    return 1;
×
683

684
  // parse required arguments
685
  if ( !parseRequiredArgument( db, args, i, "DB", "schema" ) )
20✔
686
    return 1;
1✔
687

688
  if ( i < args.size() )
4✔
689
  {
690
    // optional output argument
691
    printOutput = false;
2✔
692
    schemaJson = args[i++];
2✔
693

694
    if ( !checkNoExtraArguments( args, i, "schema" ) )
4✔
695
      return 1;
1✔
696
  }
697

698
  Context *ctx = static_cast<Context *>( context );
3✔
699
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
3✔
700
  ctx->setTablesToSkip( tables );
3✔
701

702
  std::string json;
3✔
703
  TmpFile tmpJson;
3✔
704
  if ( printOutput )
3✔
705
  {
706
    json = randomTmpFilename( );
2✔
707
    tmpJson.setPath( json );
2✔
708
  }
709
  else
710
    json = schemaJson;
1✔
711

712
  int ret = GEODIFF_schema( context, driverName.data(), driverOptions.data(), db.data(), json.data() );
3✔
713
  if ( ret != GEODIFF_SUCCESS )
3✔
714
  {
715
    std::cout << "Error: export changeset to summary failed!" << std::endl;
1✔
716
    return 1;
1✔
717
  }
718

719
  if ( printOutput )
2✔
720
  {
721
    if ( !fileToStdout( json ) )
1✔
722
    {
723
      std::cout << "Error: unable to read content of file " << json << std::endl;
×
724
      return 1;
×
725
    }
726
  }
727

728
  return 0;
2✔
729
}
5✔
730

731
static int handleCmdDump( GEODIFF_ContextH context, const std::vector<std::string> &args )
6✔
732
{
733
  // geodiff dump [OPTIONS...] DB CH_OUTPUT
734

735
  size_t i = 1;
6✔
736
  std::string db, chOutput;
6✔
737
  std::string driverName = "sqlite", driverOptions;
12✔
738
  std::string tablesToSkip;
6✔
739

740
  // parse options
741
  if ( !parseDriverOption( args, i, "dump", driverName, driverOptions, tablesToSkip ) )
12✔
742
    return 1;
2✔
743

744
  if ( !parseRequiredArgument( db, args, i, "DB", "dump" ) )
16✔
745
    return 1;
1✔
746
  if ( !parseRequiredArgument( chOutput, args, i, "CH_OUTPUT", "dump" ) )
12✔
747
    return 1;
1✔
748
  if ( !checkNoExtraArguments( args, i, "dump" ) )
4✔
749
    return 1;
1✔
750

751
  Context *ctx = static_cast<Context *>( context );
1✔
752
  std::vector<std::string> tables = parseIgnoredTables( tablesToSkip );
1✔
753
  ctx->setTablesToSkip( tables );
1✔
754

755
  int ret = GEODIFF_dumpData( context, driverName.data(), driverOptions.data(), db.data(), chOutput.data() );
1✔
756
  if ( ret != GEODIFF_SUCCESS )
1✔
757
  {
758
    std::cout << "Error: dump database failed!" << std::endl;
×
759
    return 1;
×
760
  }
761

762
  return 0;
1✔
763
}
6✔
764

765
static int handleCmdDrivers( const std::vector<std::string> &args )
2✔
766
{
767
  // geodiff drivers
768

769
  size_t i = 1;
2✔
770
  if ( !checkNoExtraArguments( args, i, "drivers" ) )
4✔
771
    return 1;
1✔
772

773
  for ( const std::string &driverName : Driver::drivers() )
3✔
774
  {
775
    std::cout << driverName << std::endl;
2✔
776
  }
1✔
777

778
  return 0;
1✔
779
}
780

781
static int handleCmdVersion( const std::vector<std::string> &args )
3✔
782
{
783
  size_t i = 1;
3✔
784
  if ( !checkNoExtraArguments( args, i, "version" ) )
6✔
785
    return 1;
1✔
786

787
  std::cout << GEODIFF_version() << std::endl;
2✔
788
  return 0;
2✔
789
}
790

791
static GEODIFF_LoggerLevel getGeodiffLoggerLevel( )
104✔
792
{
793
  return ( GEODIFF_LoggerLevel ) getEnvVarInt( "GEODIFF_LOGGER_LEVEL", ( int ) LevelWarning );
208✔
794
}
795

796
static int handleCmdHelp( const std::vector<std::string> &args )
2✔
797
{
798
  size_t i = 1;
2✔
799
  if ( !checkNoExtraArguments( args, i, "help" ) )
4✔
800
    return 1;
1✔
801

802

803
  std::cout << "GEODIFF " << GEODIFF_version() << ", a tool for handling diffs for geospatial data.\n\
1✔
804
\n\
805
Usage: geodiff <command> [args...]\n\
806
\n\
807
You can control verbosity using the environment variable GEODIFF_LOGGER_LEVEL:\n\
808
    0 = Nothing, 1 = Errors, 2 = Warnings, 3 = Info, 4 = Debug\n\
809
    (The default is 2 - showing only errors and warnings.)\n\
810
\n\
811
In the commands listed below, database files may be any GeoPackage files or other\n\
812
kinds of SQLite database files. This is using the default 'sqlite' driver. Even\n\
813
when 'sqlite' driver is specified in a command with --driver option, there are\n\
814
no extra driver options it needs (empty string \"\" can be passed).\n\
815
\n\
816
There may be other drivers available, for example 'postgres' driver. Its driver\n\
817
options expect the connection string as understood by its client library - either\n\
818
key/value pairs (e.g. \"host=localhost port=5432 dbname=mydb\") or connection URI\n\
819
(e.g. \"postgresql://localhost:5432/mydb\").\n\
820
\n\
821
Create and apply changesets (diffs):\n\
822
\n\
823
  geodiff diff [OPTIONS...] DB_1 DB_2 [CH_OUTPUT]\n\
824
\n\
825
    Creates a changeset (diff) between databases DB_BASE and DB_MODIFIED. If CH_OUTPUT\n\
826
    is specified, the result is written to that file, otherwise the output goes\n\
827
    to the standard output. By default, the changeset is written in the binary\n\
828
    format.\n\
829
\n\
830
    Options:\n\
831
      --json          Write changeset in JSON format instead of binary\n\
832
      --summary       Write only a summary for each table (in JSON)\n\
833
      --driver NAME DRIVER_OPTIONS\n\
834
                      Use driver NAME instead of the default 'sqlite' for both\n\
835
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
836
      --driver-1 NAME DRIVER_OPTIONS\n\
837
                      Use driver NAME just for the first database. This allows creation\n\
838
                      of changesets across datasets in two different drivers.\n\
839
      --driver-2 NAME DRIVER_OPTIONS\n\
840
                      Use driver NAME just for the second database. This allows\n\
841
                      creation of changesets across datasets in two different drivers.\n\
842
      --skip-tables TABLES\n\
843
                      Ignore specified tables when creating a changeset. Tables are defined as\n\
844
                      a semicolon separated list of names.\n\
845
\n\
846
  geodiff apply [OPTIONS...] DB CH_INPUT\n\
847
\n\
848
    Applies a changeset (diff) from file CH_INPUT to the database file DB.\n\
849
    The changeset must be in the binary format (JSON format is not supported).\n\
850
\n\
851
    Options:\n\
852
      --driver NAME DRIVER_OPTIONS\n\
853
                      Use driver NAME instead of the default 'sqlite' for the\n\
854
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
855
      --skip-tables TABLES\n\
856
                      Ignore specified tables when applying a changeset. Tables are defined as\n\
857
                      a semicolon separated list of names.\n\
858
\n\
859
Rebasing:\n\
860
\n\
861
  geodiff rebase-diff [OPTIONS...] DB_BASE CH_BASE_OUR CH_BASE_THEIR CH_REBASED CONFLICT\n\
862
\n\
863
    Creates a rebased changeset. Using DB_BASE as the common base for \"our\" local\n\
864
    changes (CH_BASE_OUR) and \"their\" changes (CH_BASE_THEIR), the command will take\n\
865
    \"our\" changes and rebase them on top of \"their\" changes, and write results\n\
866
    to CH_REBASED file (containing just \"our\" changes, but modified to apply cleanly\n\
867
    on top of \"their\" changes). As a result, taking DB_BASE, applying CH_BASE_THEIR\n\
868
    and then applying CH_REBASED will result in a database containing both \"our\" and\n\
869
    \"their\" changes. If there were any conflicts during the rebase, they will be\n\
870
    written to CONFLICT file (in JSON format).\n\
871
\n\
872
    Options:\n\
873
      --driver NAME DRIVER_OPTIONS\n\
874
                      Use driver NAME instead of the default 'sqlite' for both\n\
875
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
876
      --skip-tables TABLES\n\
877
                      Ignore specified tables when creating a rebased changeset. Tables are\n\
878
                      defined as a semicolon separated list of names.\n\
879
\n\
880
  geodiff rebase-db [OPTIONS...] DB_BASE DB_OUR CH_BASE_THEIR CONFLICT\n\
881
\n\
882
    Rebases database DB_OUR, using DB_BASE as the common base and CH_BASE_THEIR as the other\n\
883
    source of changes. CH_BASE_THEIR is a changeset containing changes between DB_BASE and\n\
884
    some other database. This will cause DB_OUR to be updated in-place to contain changes\n\
885
    (DB_OUR - DB_BASE) rebased on top of CH_BASE_THEIR. After successful rebase, DB_OUR will\n\
886
    contain both \"our\" and \"their\" changes. If there were any conflicts during\n\
887
    the rebase, they will be written to CONFLICT file (in JSON format).\n\
888
\n\
889
    Options:\n\
890
      --driver NAME DRIVER_OPTIONS\n\
891
                      Use driver NAME instead of the default 'sqlite' for all three\n\
892
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
893
      --skip-tables TABLES\n\
894
                      Ignore specified tables when rebasing. Tables are defined as\n\
895
                      a semicolon separated list of names.\n\
896
\n\
897
Utilities:\n\
898
\n\
899
  geodiff invert CH_INPUT CH_OUTPUT\n\
900
\n\
901
    Inverts changeset in file CH_INPUT and writes inverted changeset to CH_OUTPUT.\n\
902
    Both input and output changesets are in the binary format.\n\
903
\n\
904
  geodiff concat CH_INPUT_1 CH_INPUT_2 [...] CH_OUTPUT\n\
905
\n\
906
    Concatenates two or more changeset files (CH_INPUT_1, CH_INPUT_2, ...) into a\n\
907
    single changeset. During concatenation, commands that act on the same rows get\n\
908
    merged together.\n\
909
\n\
910
  geodiff as-json CH_INPUT [CH_OUTPUT]\n\
911
\n\
912
    Converts the changeset in CH_INPUT file (in binary format) to JSON representation.\n\
913
    If CH_OUTPUT file is provided, it will be written to that file, otherwise it will\n\
914
    be written to the standard output.\n\
915
\n\
916
  geodiff as-summary CH_INPUT [SUMMARY]\n\
917
\n\
918
    Converts the changeset in CH_INPUT file (in binary format) to a summary JSON\n\
919
    which only contains overall counts of insert/update/delete commands for each table.\n\
920
\n\
921
  geodiff copy [OPTIONS...] DB_SOURCE DB_DESTINATION\n\
922
\n\
923
    Copies the source database DB_SOURCE to the destination database DB_DESTINATION.\n\
924
\n\
925
    Options:\n\
926
      --driver NAME DRIVER_OPTIONS\n\
927
                      Use driver NAME instead of the default 'sqlite' for both\n\
928
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
929
      --driver-1 NAME DRIVER_OPTIONS\n\
930
                      Use driver NAME just for the first database. This allows creation\n\
931
                      of changesets across datasets in two different drivers.\n\
932
      --driver-2 NAME DRIVER_OPTIONS\n\
933
                      Use driver NAME just for the second database. This allows\n\
934
                      creation of changesets across datasets in two different drivers.\n\
935
      --skip-tables TABLES\n\
936
                      Ignore specified tables when copying the database. Tables are defined\n\
937
                      as a semicolon separated list of names.\n\
938
\n\
939
  geodiff schema [OPTIONS...] DB [SCHEMA_JSON]\n\
940
\n\
941
    Writes database schema of DB as understood by geodiff as JSON. If SCHEMA_JSON file\n\
942
    is provided, the output will be written to the file, otherwise the standard output\n\
943
    will be used.\n\
944
\n\
945
    Options:\n\
946
      --driver NAME DRIVER_OPTIONS\n\
947
                      Use driver NAME instead of the default 'sqlite' for the\n\
948
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
949
      --skip-tables TABLES\n\
950
                      Ignore specified tables when writing a schema. Tables are defined\n\
951
                      as a semicolon separated list of names.\n\
952
\n\
953
  geodiff dump [OPTIONS...] DB CH_OUTPUT\n\
954
\n\
955
    Dumps content of database DB to a changeset as a series of \"insert\" commands.\n\
956
\n\
957
    Options:\n\
958
      --driver NAME DRIVER_OPTIONS\n\
959
                      Use driver NAME instead of the default 'sqlite' for the\n\
960
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
961
      --skip-tables TABLES\n\
962
                      Ignore specified tables when dumping the database content. Tables\n\
963
                      are defined a semicolon separated list of names.\n\
964
\n\
965
  geodiff drivers\n\
966
\n\
967
    Prints the list of all drivers supported in this version. The \"sqlite\" driver\n\
968
    is always available.\n\
969
\n\
970
  geodiff version\n\
971
\n\
972
    Prints version of geodiff.\n\
973
\n\
974
  geodiff help\n\
975
\n\
976
    Prints this help information.\n\
977
\n\
978
Copyright (C) 2019-2023 Lutra Consulting\n\
979
";
980
  return 0;
1✔
981
}
982

983
class GeodiffContext
984
{
985
  public:
986
    GeodiffContext()
104✔
987
    {
104✔
988
      mContext = GEODIFF_createContext();
104✔
989
      GEODIFF_CX_setMaximumLoggerLevel( mContext,  getGeodiffLoggerLevel() );
104✔
990
    }
104✔
991
    ~GeodiffContext()
104✔
992
    {
993
      if ( mContext )
104✔
994
      {
995
        GEODIFF_CX_destroy( mContext );
104✔
996
        mContext = nullptr;
104✔
997
      }
998
    }
104✔
999
    GEODIFF_ContextH handle() {return mContext;}
95✔
1000

1001
  private:
1002
    GEODIFF_ContextH mContext = nullptr;
1003
};
1004

1005

1006
int main( int argc, char *argv[] )
104✔
1007
{
1008
  GeodiffContext context;
104✔
1009

1010
  std::vector<std::string> args;
104✔
1011
  for ( int i = 1; i < argc; ++i )
446✔
1012
    args.push_back( argv[i] );
684✔
1013

1014
  if ( args.size() < 1 )
104✔
1015
  {
1016
    std::cout << "Error: missing command. See 'geodiff help' for a list of commands." << std::endl;
1✔
1017
    return 1;
1✔
1018
  }
1019

1020
  std::string command = args[0];
103✔
1021
  if ( command == "diff" )
103✔
1022
  {
1023
    return handleCmdDiff( context.handle(), args );
18✔
1024
  }
1025
  else if ( command == "apply" )
85✔
1026
  {
1027
    return handleCmdApply( context.handle(), args );
13✔
1028
  }
1029
  else if ( command == "rebase-diff" )
72✔
1030
  {
1031
    return handleCmdRebaseDiff( context.handle(),  args );
10✔
1032
  }
1033
  else if ( command == "rebase-db" )
62✔
1034
  {
1035
    return handleCmdRebaseDb( context.handle(),  args );
9✔
1036
  }
1037
  else if ( command == "invert" )
53✔
1038
  {
1039
    return handleCmdInvert( context.handle(),  args );
6✔
1040
  }
1041
  else if ( command == "concat" )
47✔
1042
  {
1043
    return handleCmdConcat( context.handle(), args );
4✔
1044
  }
1045
  else if ( command == "as-json" )
43✔
1046
  {
1047
    return handleCmdAsJson( context.handle(),  args );
8✔
1048
  }
1049
  else if ( command == "as-summary" )
35✔
1050
  {
1051
    return handleCmdAsSummary( context.handle(),  args );
4✔
1052
  }
1053
  else if ( command == "copy" )
31✔
1054
  {
1055
    return handleCmdCopy( context.handle(), args );
12✔
1056
  }
1057
  else if ( command == "schema" )
19✔
1058
  {
1059
    return handleCmdSchema( context.handle(),  args );
5✔
1060
  }
1061
  else if ( command == "dump" )
14✔
1062
  {
1063
    return handleCmdDump( context.handle(), args );
6✔
1064
  }
1065
  else if ( command == "drivers" )
8✔
1066
  {
1067
    return handleCmdDrivers( args );
2✔
1068
  }
1069
  else if ( command == "version" )
6✔
1070
  {
1071
    return handleCmdVersion( args );
3✔
1072
  }
1073
  else if ( command == "help" )
3✔
1074
  {
1075
    return handleCmdHelp( args );
2✔
1076
  }
1077
  else
1078
  {
1079
    std::cout << "Error: unknown command '" << command << "'. See 'geodiff help' for a list of commands." << std::endl;
1✔
1080
    return 1;
1✔
1081
  }
1082
}
104✔
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