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

MerginMaps / geodiff / 20333063629

18 Dec 2025 09:58AM UTC coverage: 88.012%. First build
20333063629

Pull #235

github

web-flow
Merge 5698a75df into ff6ae36a1
Pull Request #235: Fix applying changeset to DB with constraints on Postgres

87 of 125 new or added lines in 4 files covered. (69.6%)

3612 of 4104 relevant lines covered (88.01%)

576.89 hits per line

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

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

6
#include "postgresdriver.h"
7

8
#include "driver.h"
9
#include "geodifflogger.hpp"
10
#include "geodiffutils.hpp"
11
#include "changeset.h"
12
#include "changesetreader.h"
13
#include "changesetutils.h"
14
#include "changesetwriter.h"
15
#include "postgresutils.h"
16
#include "sqliteutils.h"
17
#include "geodiffcontext.hpp"
18

19
#include <algorithm>
20
#include <iostream>
21
#include <memory.h>
22

23

24
/**
25
 * Wrapper around PostgreSQL transactions.
26
 *
27
 * Constructor start a trasaction, it needs to be confirmed by a call to commitChanges() when
28
 * changes are ready to be written. If commitChanges() is not called, changes since the constructor
29
 * will be rolled back (so that on exception everything gets cleaned up properly).
30
 */
31
class PostgresTransaction
32
{
33
  public:
34
    explicit PostgresTransaction( PGconn *conn )
24✔
35
      : mConn( conn )
24✔
36
    {
37
      PostgresResult res( execSql( mConn, "BEGIN" ) );
24✔
38
      if ( res.status() != PGRES_COMMAND_OK )
24✔
39
        throw GeoDiffException( "Unable to start transaction" );
×
40
    }
24✔
41

42
    ~PostgresTransaction()
24✔
43
    {
44
      if ( mConn )
24✔
45
      {
46
        // we had some problems - roll back any pending changes
47
        PostgresResult res( execSql( mConn, "ROLLBACK" ) );
1✔
48
      }
1✔
49
    }
24✔
50

51
    void commitChanges()
23✔
52
    {
53
      assert( mConn );
23✔
54
      PostgresResult res( execSql( mConn, "COMMIT" ) );
23✔
55
      if ( res.status() != PGRES_COMMAND_OK )
23✔
56
        throw GeoDiffException( "Unable to commit transaction" );
×
57

58
      // reset handler to the database so that the destructor does nothing
59
      mConn = nullptr;
23✔
60
    }
23✔
61

62
  private:
63
    PGconn *mConn = nullptr;
64
};
65

66
/////
67

68
void PostgresDriver::logApplyConflict( const std::string &type, const ChangesetEntry &entry ) const
×
69
{
70
  context()->logger().warn( "CONFLICT: " + type + ":\n" + changesetEntryToJSON( entry ).dump( 2 ) );
×
71
}
×
72

73
PostgresDriver::PostgresDriver( const Context *context )
63✔
74
  : Driver( context )
63✔
75
{
76
}
63✔
77

78
PostgresDriver::~PostgresDriver()
126✔
79
{
80
  close();
63✔
81
}
126✔
82

83
void PostgresDriver::openPrivate( const DriverParametersMap &conn )
63✔
84
{
85
  DriverParametersMap::const_iterator connInfo = conn.find( "conninfo" );
63✔
86
  if ( connInfo == conn.end() )
63✔
87
    throw GeoDiffException( "Missing 'conninfo' parameter" );
×
88
  std::string connInfoStr = connInfo->second;
63✔
89

90
  DriverParametersMap::const_iterator baseSchema = conn.find( "base" );
63✔
91
  if ( baseSchema == conn.end() )
63✔
92
    throw GeoDiffException( "Missing 'base' parameter" );
×
93
  mBaseSchema = baseSchema->second;
63✔
94

95
  DriverParametersMap::const_iterator modifiedSchema = conn.find( "modified" );
63✔
96
  mModifiedSchema = ( modifiedSchema == conn.end() ) ? std::string() : modifiedSchema->second;
63✔
97

98
  if ( mConn )
63✔
99
    throw GeoDiffException( "Connection already opened" );
×
100

101
  PGconn *c = PQconnectdb( connInfoStr.c_str() );
63✔
102

103
  if ( PQstatus( c ) != CONNECTION_OK )
63✔
104
  {
105
    throw GeoDiffException( "Cannot connect to PostgreSQL database: " + std::string( PQerrorMessage( c ) ) );
×
106
  }
107

108
  mConn = c;
63✔
109

110
  // Make sure we are using enough digits for floating point numbers to make sure that we are
111
  // not loosing any digits when querying data.
112
  // https://www.postgresql.org/docs/12/runtime-config-client.html#GUC-EXTRA-FLOAT-DIGITS
113
  PostgresResult res( execSql( mConn, "SET extra_float_digits = 2;" ) );
63✔
114
  if ( res.status() != PGRES_COMMAND_OK )
63✔
115
    throw GeoDiffException( "Failed to set extra_float_digits" );
×
116
}
63✔
117

118
void PostgresDriver::close()
63✔
119
{
120
  mBaseSchema.clear();
63✔
121
  mModifiedSchema.clear();
63✔
122
  if ( mConn )
63✔
123
    PQfinish( mConn );
63✔
124
  mConn = nullptr;
63✔
125
}
63✔
126

127
void PostgresDriver::open( const DriverParametersMap &conn )
47✔
128
{
129
  openPrivate( conn );
47✔
130

131
  {
132
    PostgresResult resBase( execSql( mConn, "SELECT 1 FROM pg_namespace WHERE nspname = " + quotedString( mBaseSchema ) ) );
47✔
133
    if ( resBase.rowCount() == 0 )
47✔
134
    {
135
      std::string baseSchema = mBaseSchema;  // close() will erase mBaseSchema...
×
136
      close();
×
137
      throw GeoDiffException( "The base schema does not exist: " + baseSchema );
×
138
    }
×
139
  }
47✔
140

141
  if ( !mModifiedSchema.empty() )
47✔
142
  {
143
    PostgresResult resBase( execSql( mConn, "SELECT 1 FROM pg_namespace WHERE nspname = " + quotedString( mModifiedSchema ) ) );
16✔
144
    if ( resBase.rowCount() == 0 )
16✔
145
    {
146
      std::string modifiedSchema = mModifiedSchema;  // close() will erase mModifiedSchema...
×
147
      close();
×
148
      throw GeoDiffException( "The base schema does not exist: " + modifiedSchema );
×
149
    }
×
150
  }
16✔
151
}
47✔
152

153
void PostgresDriver::create( const DriverParametersMap &conn, bool overwrite )
16✔
154
{
155
  openPrivate( conn );
16✔
156

157
  std::string sql;
16✔
158
  if ( overwrite )
16✔
159
    sql += "DROP SCHEMA IF EXISTS " + quotedIdentifier( mBaseSchema ) + " CASCADE; ";
15✔
160
  sql += "CREATE SCHEMA " + quotedIdentifier( mBaseSchema ) + ";";
16✔
161

162
  PostgresResult res( execSql( mConn, sql ) );
16✔
163
  if ( res.status() != PGRES_COMMAND_OK )
16✔
164
    throw GeoDiffException( "Failure creating table: " + res.statusErrorMessage() );
×
165
}
16✔
166

167

168
std::vector<std::string> PostgresDriver::listTables( bool useModified )
69✔
169
{
170
  if ( !mConn )
69✔
NEW
171
    throw GeoDiffException( "Not connected to a database" );
×
172
  if ( useModified && mModifiedSchema.empty() )
69✔
173
    throw GeoDiffException( "Should use modified schema, but it was not set" );
×
174

175
  std::string schemaName = useModified ? mModifiedSchema : mBaseSchema;
69✔
176
  std::string sql = "select tablename from pg_tables where schemaname=" + quotedString( schemaName );
69✔
177
  PostgresResult res( execSql( mConn, sql ) );
69✔
178

179
  std::vector<std::string> tables;
69✔
180
  for ( int i = 0; i < res.rowCount(); ++i )
169✔
181
  {
182
    if ( startsWith( res.value( i, 0 ), "gpkg_" ) )
200✔
183
      continue;
×
184

185
    if ( context()->isTableSkipped( res.value( i, 0 ) ) )
100✔
186
      continue;
×
187

188
    tables.push_back( res.value( i, 0 ) );
100✔
189
  }
190

191
  // make sure tables are in alphabetical order, so that if we compare table names
192
  // from different schemas, they should be matching
193
  std::sort( tables.begin(), tables.end() );
69✔
194

195
  return tables;
138✔
196
}
69✔
197

198
struct GeometryTypeDetails
199
{
200
  // cppcheck-suppress unusedStructMember
201
  const char *flatType;
202
  // cppcheck-suppress unusedStructMember
203
  bool hasZ;
204
  // cppcheck-suppress unusedStructMember
205
  bool hasM;
206
};
207

208
static void extractGeometryTypeDetails( const std::string &geomType, const std::string &coordinateDimension, std::string &flatGeomType, bool &hasZ, bool &hasM )
132✔
209
{
210
  std::map<std::string, GeometryTypeDetails> d =
211
  {
212
    { "POINT",   { "POINT", false, false } },
213
    { "POINTZ",  { "POINT", true,  false } },
214
    { "POINTM",  { "POINT", false, true  } },
215
    { "POINTZM", { "POINT", true,  true  } },
216
    { "LINESTRING",   { "LINESTRING", false, false } },
217
    { "LINESTRINGZ",  { "LINESTRING", true,  false } },
218
    { "LINESTRINGM",  { "LINESTRING", false, true  } },
219
    { "LINESTRINGZM", { "LINESTRING", true,  true  } },
220
    { "POLYGON",   { "POLYGON", false, false } },
221
    { "POLYGONZ",  { "POLYGON", true,  false } },
222
    { "POLYGONM",  { "POLYGON", false, true  } },
223
    { "POLYGONZM", { "POLYGON", true,  true  } },
224

225
    { "MULTIPOINT",   { "MULTIPOINT", false, false } },
226
    { "MULTIPOINTZ",  { "MULTIPOINT", true,  false } },
227
    { "MULTIPOINTM",  { "MULTIPOINT", false, true  } },
228
    { "MULTIPOINTZM", { "MULTIPOINT", true,  true  } },
229
    { "MULTILINESTRING",   { "MULTILINESTRING", false, false } },
230
    { "MULTILINESTRINGZ",  { "MULTILINESTRING", true,  false } },
231
    { "MULTILINESTRINGM",  { "MULTILINESTRING", false, true  } },
232
    { "MULTILINESTRINGZM", { "MULTILINESTRING", true,  true  } },
233
    { "MULTIPOLYGON",   { "MULTIPOLYGON", false, false } },
234
    { "MULTIPOLYGONZ",  { "MULTIPOLYGON", true,  false } },
235
    { "MULTIPOLYGONM",  { "MULTIPOLYGON", false, true  } },
236
    { "MULTIPOLYGONZM", { "MULTIPOLYGON", true,  true  } },
237

238
    { "GEOMETRYCOLLECTION",   { "GEOMETRYCOLLECTION", false, false } },
239
    { "GEOMETRYCOLLECTIONZ",  { "GEOMETRYCOLLECTION", true,  false } },
240
    { "GEOMETRYCOLLECTIONM",  { "GEOMETRYCOLLECTION", false, true  } },
241
    { "GEOMETRYCOLLECTIONZM", { "GEOMETRYCOLLECTION", true,  true  } },
242

243
    // TODO: curve geometries
244
  };
3,960✔
245

246
  /*
247
   *  Special PostGIS coding of xyZ, xyM and xyZM type https://postgis.net/docs/using_postgis_dbmanagement.html#geometry_columns
248
   *  coordinateDimension number bears information about third and fourth dimension.
249
   */
250
  std::string type = geomType;
132✔
251

252
  if ( coordinateDimension == "4" )
132✔
253
  {
254
    type += "ZM";
18✔
255
  }
256
  else if ( ( coordinateDimension == "3" ) && ( type.back() != 'M' ) )
114✔
257
  {
258
    type += "Z";
18✔
259
  }
260

261
  auto it = d.find( type );
132✔
262
  if ( it != d.end() )
132✔
263
  {
264
    flatGeomType = it->second.flatType;
132✔
265
    hasZ = it->second.hasZ;
132✔
266
    hasM = it->second.hasM;
132✔
267
  }
268
  else
269
    throw GeoDiffException( "Unknown geometry type: " + type );
×
270
}
660✔
271

272

273
TableSchema PostgresDriver::tableSchema( const std::string &tableName, bool useModified )
145✔
274
{
275
  if ( !mConn )
145✔
276
    throw GeoDiffException( "Not connected to a database" );
×
277
  if ( useModified && mModifiedSchema.empty() )
145✔
278
    throw GeoDiffException( "Should use modified schema, but it was not set" );
×
279

280
  std::string schemaName = useModified ? mModifiedSchema : mBaseSchema;
145✔
281

282
  // try to figure out details of the geometry columns (if any)
283
  std::string sqlGeomDetails = "SELECT f_geometry_column, type, srid, coord_dimension FROM geometry_columns WHERE f_table_schema = " +
290✔
284
                               quotedString( schemaName ) + " AND f_table_name = " + quotedString( tableName );
435✔
285
  std::map<std::string, std::pair<std::string, std::string>> geomTypes;
145✔
286
  std::map<std::string, int> geomSrids;
145✔
287
  PostgresResult resGeomDetails( execSql( mConn, sqlGeomDetails ) );
145✔
288
  for ( int i = 0; i < resGeomDetails.rowCount(); ++i )
277✔
289
  {
290
    std::string name = resGeomDetails.value( i, 0 );
132✔
291
    std::string type = resGeomDetails.value( i, 1 );
132✔
292
    std::string srid = resGeomDetails.value( i, 2 );
132✔
293
    std::string dimension = resGeomDetails.value( i, 3 );
132✔
294
    int sridInt = srid.empty() ? -1 : atoi( srid.c_str() );
132✔
295
    geomTypes[name] = { type, dimension };
132✔
296
    geomSrids[name] = sridInt;
132✔
297
  }
132✔
298

299
  std::string sqlColumns =
300
    "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), i.indisprimary, a.attnotnull, "
301
    "    EXISTS ("
302
    "             SELECT FROM pg_attrdef ad"
303
    "             WHERE  ad.adrelid = a.attrelid"
304
    "             AND    ad.adnum   = a.attnum"
305
    "             AND    pg_get_expr(ad.adbin, ad.adrelid)"
306
    "                  = 'nextval('''"
307
    "                 || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass"
308
    "                 || '''::regclass)'"
309
    "       ) AS has_sequence"
310
    " FROM pg_catalog.pg_attribute a"
311
    " LEFT JOIN pg_index i ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)"
312
    " WHERE"
313
    "   a.attnum > 0"
314
    "   AND NOT a.attisdropped"
315
    "   AND a.attrelid = ("
316
    "      SELECT c.oid"
317
    "        FROM pg_catalog.pg_class c"
318
    "        LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace"
319
    "        WHERE c.relname = " + quotedString( tableName ) +
290✔
320
    "          AND n.nspname = " + quotedString( schemaName ) +
580✔
321
    "   )"
322
    "  ORDER BY a.attnum";
145✔
323

324
  PostgresResult res( execSql( mConn, sqlColumns ) );
145✔
325

326
  int srsId = -1;
145✔
327
  TableSchema schema;
145✔
328
  schema.name = tableName;
145✔
329
  for ( int i = 0; i < res.rowCount(); ++i )
755✔
330
  {
331
    TableColumnInfo col;
610✔
332
    col.name = res.value( i, 0 );
610✔
333
    col.isPrimaryKey = ( res.value( i, 2 ) == "t" );
610✔
334
    std::string type( res.value( i, 1 ) );
610✔
335

336
    if ( startsWith( type, "geometry" ) )
1,220✔
337
    {
338
      std::string geomTypeName;
132✔
339
      bool hasM = false;
132✔
340
      bool hasZ = false;
132✔
341

342
      if ( geomTypes.find( col.name ) != geomTypes.end() )
132✔
343
      {
344
        extractGeometryTypeDetails( geomTypes[col.name].first, geomTypes[col.name].second, geomTypeName, hasZ, hasM );
132✔
345
        srsId = geomSrids[col.name];
132✔
346
      }
347
      col.setGeometry( geomTypeName, srsId, hasM, hasZ );
132✔
348
    }
132✔
349

350
    col.type = columnType( context(), type, Driver::POSTGRESDRIVERNAME, col.isGeometry );
610✔
351
    col.isNotNull = ( res.value( i, 3 ) == "t" );
610✔
352
    col.isAutoIncrement = ( res.value( i, 4 ) == "t" );
610✔
353

354
    schema.columns.push_back( col );
610✔
355
  }
610✔
356

357
  //
358
  // get CRS details
359
  //
360

361
  if ( srsId != -1 )
145✔
362
  {
363
    PostgresResult resCrs( execSql( mConn,
364
                                    "SELECT auth_name, auth_srid, srtext "
365
                                    "FROM spatial_ref_sys WHERE srid = " + std::to_string( srsId ) ) );
132✔
366

367
    if ( resCrs.rowCount() == 0 )
132✔
368
      throw GeoDiffException( "Unknown CRS in table " + tableName );
×
369
    schema.crs.srsId = srsId;
132✔
370
    schema.crs.authName = resCrs.value( 0, 0 );
132✔
371
    schema.crs.authCode = atoi( resCrs.value( 0, 1 ).c_str() );
132✔
372
    schema.crs.wkt = resCrs.value( 0, 2 );
132✔
373
  }
132✔
374

375
  return schema;
290✔
376
}
145✔
377

378

379
static std::string allColumnNames( const TableSchema &tbl, const std::string &prefix = "" )
104✔
380
{
381
  std::string columns;
104✔
382
  for ( const TableColumnInfo &c : tbl.columns )
530✔
383
  {
384
    if ( !columns.empty() )
426✔
385
      columns += ", ";
322✔
386

387
    std::string name;
426✔
388
    if ( !prefix.empty() )
426✔
389
      name = prefix + ".";
158✔
390
    name += quotedIdentifier( c.name );
426✔
391

392
    if ( c.isGeometry )
426✔
393
      columns += "ST_AsBinary(" + name + ")";
91✔
394
    else if ( c.type == "timestamp without time zone" )
670✔
395
    {
396
      // by default postgresql would return date/time as a formatted string
397
      // e.g. "2020-07-13 16:17:54" but we want IS0-8601 format "2020-07-13T16:17:54.060110Z"
398
      // Our format needs to capture the full resolution of Postgres timestamps (microseconds)
399
      columns += "to_char(" + name + ",'YYYY-MM-DD\"T\"HH24:MI:SS.US\"Z\"')";
10✔
400
    }
401
    else
402
      columns += name;
325✔
403
  }
426✔
404
  return columns;
104✔
405
}
×
406

407

408
//! Constructs SQL query to get all rows that do not exist in the other table (used for insert and delete)
409
static std::string sqlFindInserted( const std::string &schemaNameBase, const std::string &schemaNameModified, const std::string &tableName, const TableSchema &tbl, bool reverse )
38✔
410
{
411
  std::string exprPk;
38✔
412
  for ( const TableColumnInfo &c : tbl.columns )
196✔
413
  {
414
    if ( c.isPrimaryKey )
158✔
415
    {
416
      if ( !exprPk.empty() )
38✔
417
        exprPk += " AND ";
×
418
      exprPk += quotedIdentifier( schemaNameBase ) + "." + quotedIdentifier( tableName ) + "." + quotedIdentifier( c.name ) + "=" +
76✔
419
                quotedIdentifier( schemaNameModified ) + "." + quotedIdentifier( tableName ) + "." + quotedIdentifier( c.name );
114✔
420
    }
421
  }
422

423
  std::string sql = "SELECT " + allColumnNames( tbl ) + " FROM " +
76✔
424
                    quotedIdentifier( reverse ? schemaNameBase : schemaNameModified ) + "." + quotedIdentifier( tableName ) +
152✔
425
                    " WHERE NOT EXISTS ( SELECT 1 FROM " +
76✔
426
                    quotedIdentifier( reverse ? schemaNameModified : schemaNameBase ) + "." + quotedIdentifier( tableName ) +
152✔
427
                    " WHERE " + exprPk + ")";
38✔
428
  return sql;
76✔
429
}
38✔
430

431

432
static std::string sqlFindModified( const std::string &schemaNameBase, const std::string &schemaNameModified, const std::string &tableName, const TableSchema &tbl )
19✔
433
{
434
  std::string exprPk;
19✔
435
  std::string exprOther;
19✔
436
  for ( const TableColumnInfo &c : tbl.columns )
98✔
437
  {
438
    if ( c.isPrimaryKey )
79✔
439
    {
440
      if ( !exprPk.empty() )
19✔
441
        exprPk += " AND ";
×
442
      exprPk += "b." + quotedIdentifier( c.name ) + "=" +
38✔
443
                "a." + quotedIdentifier( c.name );
57✔
444
    }
445
    else // not a primary key column
446
    {
447
      if ( !exprOther.empty() )
60✔
448
        exprOther += " OR ";
42✔
449

450
      std::string colBase = "b." + quotedIdentifier( c.name );
60✔
451
      std::string colModified = "a." + quotedIdentifier( c.name );
60✔
452

453
      // pg IS DISTINCT FROM operator handles comparison between null and not null values. When comparing values with basic `=` operator,
454
      // comparison 7 = NULL returns null (no rows), not false as one would expect. IS DISTINCT FROM handles this and returns false in such situations.
455
      // When comparing non-null values with IS DISTINCT FROM operator, it works just as `=` does with non-null values.
456
      exprOther += "(" + colBase + " IS DISTINCT FROM " + colModified + ")";
60✔
457
    }
60✔
458
  }
459

460
  std::string sql = "SELECT " + allColumnNames( tbl, "a" ) + ", " + allColumnNames( tbl, "b" ) + " FROM " +
76✔
461
                    quotedIdentifier( schemaNameModified ) + "." + quotedIdentifier( tableName ) + " a, " +
76✔
462
                    quotedIdentifier( schemaNameBase ) + "." + quotedIdentifier( tableName ) + " b" +
76✔
463
                    " WHERE " + exprPk;
19✔
464

465
  if ( !exprOther.empty() )
19✔
466
    sql += " AND (" + exprOther + ")";
18✔
467

468
  return sql;
38✔
469
}
19✔
470

471

472
static bool isColumnInt( const TableColumnInfo &col )
283✔
473
{
474
  return col.type == "integer" || col.type == "smallint" || col.type == "bigint";
1,541✔
475
}
476

477
static bool isColumnDouble( const TableColumnInfo &col )
173✔
478
{
479
  return col.type == "real" || col.type == "double precision" ||
865✔
480
         startsWith( col.type.dbType, "numeric" ) || startsWith( col.type.dbType, "decimal" );
1,162✔
481
}
482

483
static bool isColumnText( const TableColumnInfo &col )
156✔
484
{
485
  return col.type == "text" || startsWith( col.type.dbType, "text(" ) ||
711✔
486
         col.type == "varchar" || startsWith( col.type.dbType, "varchar(" ) ||
561✔
487
         col.type == "character varying" || startsWith( col.type.dbType, "character varying(" ) ||
557✔
488
         col.type == "char" || startsWith( col.type.dbType, "char(" ) || startsWith( col.type.dbType, "character(" ) ||
1,022✔
489
         col.type == "citext";
468✔
490
}
491

492
static bool isColumnGeometry( const TableColumnInfo &col )
67✔
493
{
494
  return col.isGeometry;
67✔
495
}
496

497
static Value resultToValue( const PostgresResult &res, int r, size_t i, const TableColumnInfo &col )
295✔
498
{
499
  Value v;
295✔
500
  if ( res.isNull( r, i ) )
295✔
501
  {
502
    v.setNull();
11✔
503
  }
504
  else
505
  {
506
    std::string valueStr = res.value( r, i );
284✔
507
    if ( col.type == "bool" || col.type == "boolean" )
1,420✔
508
    {
509
      v.setInt( valueStr == "t" );  // PostgreSQL uses 't' for true and 'f' for false
1✔
510
    }
511
    else if ( isColumnInt( col ) )
283✔
512
    {
513
      v.setInt( atol( valueStr.c_str() ) );
110✔
514
    }
515
    else if ( isColumnDouble( col ) )
173✔
516
    {
517
      v.setDouble( atof( valueStr.c_str() ) );
17✔
518
    }
519
    else if ( isColumnText( col ) || col.type == "uuid" )
312✔
520
    {
521
      v.setString( Value::TypeText, valueStr.c_str(), valueStr.size() );
79✔
522
    }
523
    else if ( col.type == "timestamp without time zone" || col.type == "date" )
367✔
524
    {
525
      v.setString( Value::TypeText, valueStr.c_str(), valueStr.size() );
10✔
526
    }
527
    else if ( isColumnGeometry( col ) )
67✔
528
    {
529
      // the value we get should have this format: "\x01234567890abcdef"
530
      if ( valueStr.size() < 2 )
67✔
531
        throw GeoDiffException( "Unexpected geometry value" );
×
532
      if ( valueStr[0] != '\\' || valueStr[1] != 'x' )
67✔
533
        throw GeoDiffException( "Unexpected prefix in geometry value" );
×
534

535
      // 1. convert from hex representation to proper binary stream
536
      std::string binString = hex2bin( valueStr.substr( 2 ) ); // chop \x prefix
67✔
537

538
      // 2. create binary header
539
      std::string binHead = createGpkgHeader( binString, col );
67✔
540

541
      // 3. copy header and body
542
      std::string gpb( binHead.size() + binString.size(), 0 );
67✔
543

544
      memcpy( &gpb[0], binHead.data(), binHead.size() );
67✔
545
      memcpy( &gpb[binHead.size()], binString.data(), binString.size() );
67✔
546

547
      v.setString( Value::TypeBlob, gpb.data(), gpb.size() );
67✔
548
    }
67✔
549
    else
550
    {
551
      // TODO: handling of other types (list, blob, ...)
552
      throw GeoDiffException( "unknown value type: " + col.type.dbType );
×
553
    }
554
  }
284✔
555
  return v;
295✔
556
}
×
557

558
static std::string valueToSql( const Value &v, const TableColumnInfo &col )
335✔
559
{
560
  if ( v.type() == Value::TypeUndefined )
335✔
561
  {
562
    throw GeoDiffException( "valueToSql: got 'undefined' value (malformed changeset?)" );
×
563
  }
564
  else if ( v.type() == Value::TypeNull )
335✔
565
  {
566
    return "NULL";
24✔
567
  }
568
  else if ( v.type() == Value::TypeInt )
323✔
569
  {
570
    if ( col.type == "boolean" )
230✔
571
      return v.getInt() ? "'t'" : "'f'";
6✔
572
    else
573
      return std::to_string( v.getInt() );
112✔
574
  }
575
  else if ( v.type() == Value::TypeDouble )
208✔
576
  {
577
    return to_string_with_max_precision( v.getDouble() );
17✔
578
  }
579
  else if ( v.type() == Value::TypeText || v.type() == Value::TypeBlob )
191✔
580
  {
581
    if ( col.isGeometry )
191✔
582
    {
583
      // handling of geometries - they are encoded with GPKG header
584
      std::string gpkgWkb = v.getString();
76✔
585
      int headerSize = parseGpkgbHeaderSize( gpkgWkb );
76✔
586
      std::string wkb( gpkgWkb.size() - headerSize, 0 );
76✔
587

588
      memcpy( &wkb[0], &gpkgWkb[headerSize], gpkgWkb.size() - headerSize );
76✔
589
      return "ST_GeomFromWKB('\\x" + bin2hex( wkb ) + "', " + std::to_string( col.geomSrsId ) + ")";
76✔
590
    }
76✔
591
    return quotedString( v.getString() );
115✔
592
  }
593
  else
594
  {
595
    throw GeoDiffException( "unexpected value" );
×
596
  }
597
}
598

599

600
static void handleInserted( const std::string &schemaNameBase, const std::string &schemaNameModified, const std::string &tableName, const TableSchema &tbl, bool reverse, PGconn *conn, ChangesetWriter &writer, bool &first )
38✔
601
{
602
  std::string sqlInserted = sqlFindInserted( schemaNameBase, schemaNameModified, tableName, tbl, reverse );
38✔
603
  PostgresResult res( execSql( conn, sqlInserted ) );
38✔
604

605
  int rows = res.rowCount();
38✔
606
  for ( int r = 0; r < rows; ++r )
51✔
607
  {
608
    if ( first )
13✔
609
    {
610
      ChangesetTable chTable = schemaToChangesetTable( tableName, tbl );
10✔
611
      writer.beginTable( chTable );
10✔
612
      first = false;
10✔
613
    }
10✔
614

615
    ChangesetEntry e;
13✔
616
    e.op = reverse ? ChangesetEntry::OpDelete : ChangesetEntry::OpInsert;
13✔
617

618
    size_t numColumns = tbl.columns.size();
13✔
619
    for ( size_t i = 0; i < numColumns; ++i )
53✔
620
    {
621
      Value v( resultToValue( res, r, i, tbl.columns[i] ) );
40✔
622
      if ( reverse )
40✔
623
        e.oldValues.push_back( v );
4✔
624
      else
625
        e.newValues.push_back( v );
36✔
626
    }
40✔
627

628
    writer.writeEntry( e );
13✔
629
  }
13✔
630
}
38✔
631

632

633
static void handleUpdated( const std::string &schemaNameBase, const std::string &schemaNameModified, const std::string &tableName, const TableSchema &tbl, PGconn *conn, ChangesetWriter &writer, bool &first )
19✔
634
{
635
  std::string sqlModified = sqlFindModified( schemaNameBase, schemaNameModified, tableName, tbl );
19✔
636
  PostgresResult res( execSql( conn, sqlModified ) );
19✔
637

638
  int rows = res.rowCount();
19✔
639
  for ( int r = 0; r < rows; ++r )
25✔
640
  {
641
    if ( first )
6✔
642
    {
643
      ChangesetTable chTable = schemaToChangesetTable( tableName, tbl );
2✔
644
      writer.beginTable( chTable );
2✔
645
      first = false;
2✔
646
    }
2✔
647

648
    /*
649
    ** Within the old.* record associated with an UPDATE change, all fields
650
    ** associated with table columns that are not PRIMARY KEY columns and are
651
    ** not modified by the UPDATE change are set to "undefined". Other fields
652
    ** are set to the values that made up the row before the UPDATE that the
653
    ** change records took place. Within the new.* record, fields associated
654
    ** with table columns modified by the UPDATE change contain the new
655
    ** values. Fields associated with table columns that are not modified
656
    ** are set to "undefined".
657
    */
658

659
    ChangesetEntry e;
6✔
660
    e.op = ChangesetEntry::OpUpdate;
6✔
661

662
    size_t numColumns = tbl.columns.size();
6✔
663
    for ( size_t i = 0; i < numColumns; ++i )
27✔
664
    {
665
      Value v1( resultToValue( res, r, i + numColumns, tbl.columns[i] ) );
21✔
666
      Value v2( resultToValue( res, r, i, tbl.columns[i] ) );
21✔
667
      bool pkey = tbl.columns[i].isPrimaryKey;
21✔
668
      bool updated = v1 != v2;
21✔
669
      e.oldValues.push_back( ( pkey || updated ) ? v1 : Value() );
34✔
670
      e.newValues.push_back( updated ? v2 : Value() );
21✔
671
    }
21✔
672

673
    writer.writeEntry( e );
6✔
674
  }
6✔
675
}
19✔
676

677

678
void PostgresDriver::createChangeset( ChangesetWriter &writer )
16✔
679
{
680
  if ( !mConn )
16✔
681
    throw GeoDiffException( "Not connected to a database" );
×
682

683
  std::vector<std::string> tablesBase = listTables( false );
16✔
684
  std::vector<std::string> tablesModified = listTables( true );
16✔
685

686
  if ( tablesBase != tablesModified )
16✔
687
  {
688
    throw GeoDiffException( "Table names are not matching between the input databases.\n"
689
                            "Base:     " + concatNames( tablesBase ) + "\n" +
×
690
                            "Modified: " + concatNames( tablesModified ) );
×
691
  }
692

693
  for ( const std::string &tableName : tablesBase )
35✔
694
  {
695
    TableSchema tbl = tableSchema( tableName );
19✔
696
    TableSchema tblNew = tableSchema( tableName, true );
19✔
697

698
    // test that table schema in the modified is the same
699
    if ( tbl != tblNew )
19✔
700
    {
701
      if ( !tbl.compareWithBaseTypes( tblNew ) )
×
702
        throw GeoDiffException( "PostgreSQL Table schemas are not the same for table: " + tableName );
×
703
    }
704

705
    if ( !tbl.hasPrimaryKey() )
19✔
706
      continue;  // ignore tables without primary key - they can't be compared properly
×
707

708
    bool first = true;
19✔
709

710
    handleInserted( mBaseSchema, mModifiedSchema, tableName, tbl, false, mConn, writer, first );  // INSERT
19✔
711
    handleInserted( mBaseSchema, mModifiedSchema, tableName, tbl, true, mConn, writer, first );   // DELETE
19✔
712
    handleUpdated( mBaseSchema, mModifiedSchema, tableName, tbl, mConn, writer, first );          // UPDATE
19✔
713
  }
19✔
714
}
16✔
715

716

717
static std::string sqlForInsert( const std::string &schemaName, const std::string &tableName, const TableSchema &tbl, const std::vector<Value> &values )
76✔
718
{
719
  /*
720
   * For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
721
   *
722
   * INSERT INTO x (a, b, c, d) VALUES (?, ?, ?, ?)
723
   */
724

725
  std::string sql;
76✔
726
  sql += "INSERT INTO " + quotedIdentifier( schemaName ) + "." + quotedIdentifier( tableName ) + " (";
76✔
727
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
376✔
728
  {
729
    if ( i > 0 )
300✔
730
      sql += ", ";
224✔
731
    sql += quotedIdentifier( tbl.columns[i].name );
300✔
732
  }
733
  sql += ") VALUES (";
76✔
734
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
376✔
735
  {
736
    if ( i > 0 )
300✔
737
      sql += ", ";
224✔
738
    sql += valueToSql( values[i], tbl.columns[i] );
300✔
739
  }
740
  sql += ")";
76✔
741
  return sql;
76✔
742
}
×
743

744

745
static std::string sqlForUpdate( const std::string &schemaName, const std::string &tableName, const TableSchema &tbl, const std::vector<Value> &oldValues, const std::vector<Value> &newValues )
6✔
746
{
747
  std::string sql;
6✔
748
  sql += "UPDATE " + quotedIdentifier( schemaName ) + "." + quotedIdentifier( tableName ) + " SET ";
6✔
749

750
  bool first = true;
6✔
751
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
26✔
752
  {
753
    if ( newValues[i].type() != Value::TypeUndefined )
20✔
754
    {
755
      if ( !first )
9✔
756
        sql += ", ";
3✔
757
      first = false;
9✔
758
      sql += quotedIdentifier( tbl.columns[i].name ) + " = " + valueToSql( newValues[i], tbl.columns[i] );
9✔
759
    }
760
  }
761
  first = true;
6✔
762
  sql += " WHERE ";
6✔
763
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
26✔
764
  {
765
    if ( oldValues[i].type() != Value::TypeUndefined )
20✔
766
    {
767
      if ( !first )
15✔
768
        sql += " AND ";
9✔
769
      first = false;
15✔
770
      sql += quotedIdentifier( tbl.columns[i].name );
15✔
771
      if ( oldValues[i].type() == Value::TypeNull )
15✔
772
        sql += " IS NULL";
1✔
773
      else
774
        sql += " = " + valueToSql( oldValues[i], tbl.columns[i] );
14✔
775
    }
776
  }
777

778
  return sql;
6✔
779
}
×
780

781

782
static std::string sqlForDelete( const std::string &schemaName, const std::string &tableName, const TableSchema &tbl, const std::vector<Value> &values )
3✔
783
{
784
  std::string sql;
3✔
785
  sql += "DELETE FROM " + quotedIdentifier( schemaName ) + "." + quotedIdentifier( tableName ) + " WHERE ";
3✔
786
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
15✔
787
  {
788
    if ( i > 0 )
12✔
789
      sql += " AND ";
9✔
790
    if ( tbl.columns[i].isPrimaryKey )
12✔
791
      sql += quotedIdentifier( tbl.columns[i].name ) + " = " + valueToSql( values[i], tbl.columns[i] );
3✔
792
    else
793
    {
794
      if ( values[i].type() == Value::TypeNull )
9✔
795
        sql += quotedIdentifier( tbl.columns[i].name ) + " IS NULL";
×
796
      else
797
        sql += quotedIdentifier( tbl.columns[i].name ) + " = " + valueToSql( values[i], tbl.columns[i] ) ;
9✔
798
    }
799
  }
800
  return sql;
3✔
801
}
×
802

803
static bool isIntegrityError( const PostgresResult &res )
1✔
804
{
805
  return res.sqlState().substr( 0, 2 ) == "23";
1✔
806
}
807

808
ChangeApplyResult PostgresDriver::applyChange( PostgresChangeApplyState &state, const ChangesetEntry &entry )
86✔
809
{
810
  std::string tableName = entry.table->name;
86✔
811

812
  // skip any changes to GPKG meta tables
813
  if ( startsWith( tableName, "gpkg_" ) )
172✔
814
    return ChangeApplyResult::Skipped;
1✔
815

816
  // skip table if asked to
817
  if ( context()->isTableSkipped( tableName ) )
85✔
NEW
818
    return ChangeApplyResult::Skipped;
×
819

820
  if ( state.tableState.count( tableName ) == 0 )
85✔
821
  {
822
    TableSchema schema = tableSchema( tableName );
44✔
823

824
    if ( schema.columns.size() == 0 )
44✔
825
      throw GeoDiffException( "No such table: " + tableName );
×
826

827
    if ( schema.columns.size() != entry.table->columnCount() )
44✔
828
      throw GeoDiffException( "Wrong number of columns for table: " + tableName );
×
829

830
    for ( size_t i = 0; i < entry.table->columnCount(); ++i )
232✔
831
    {
832
      if ( schema.columns[i].isPrimaryKey != entry.table->primaryKeys[i] )
188✔
NEW
833
        throw GeoDiffException( "Mismatch of primary keys in table: " + tableName );
×
834
    }
835

836
    PostgresChangeApplyState::TableState &tbl = state.tableState[tableName];
44✔
837
    tbl.schema = schema;
44✔
838
    // if a table has auto-incrementing pkey (using SEQUENCE object), we may need
839
    // to update the sequence value after doing some inserts (or subsequent INSERTs would fail)
840
    std::string seqName = getSequenceObjectName( tbl.schema, tbl.autoIncrementPkeyIndex );
44✔
841
    if ( tbl.autoIncrementPkeyIndex != -1 )
44✔
842
      tbl.sequenceName = seqName;
44✔
843
  }
44✔
844
  PostgresChangeApplyState::TableState &tbl = state.tableState[tableName];
85✔
845

846
  // Create savepoint so we have somewhere to rollback to if the command fails
847
  execSql( mConn, "SAVEPOINT geodiff_apply" );
85✔
848

849
  try
850
  {
851
    if ( entry.op == ChangesetEntry::OpInsert )
85✔
852
    {
853
      std::string sql = sqlForInsert( mBaseSchema, tableName, tbl.schema, entry.newValues );
76✔
854
      PostgresResult res( execSql( mConn, sql ) );
76✔
855
      if ( res.affectedRows() != "1" )
76✔
856
        throw GeoDiffException( "Wrong number of affected rows! Expected 1, got: " + res.affectedRows() );
×
857

858
      if ( tbl.autoIncrementPkeyIndex != -1 )
76✔
859
      {
860
        int64_t pkey = entry.newValues[tbl.autoIncrementPkeyIndex].getInt();
76✔
861
        tbl.autoIncrementMax = std::max( tbl.autoIncrementMax, pkey );
76✔
862
      }
863
    }
76✔
864
    else if ( entry.op == ChangesetEntry::OpUpdate )
9✔
865
    {
866
      std::string sql = sqlForUpdate( mBaseSchema, tableName, tbl.schema, entry.oldValues, entry.newValues );
6✔
867
      PostgresResult res( execSql( mConn, sql ) );
6✔
868
      if ( res.affectedRows() != "1" )
6✔
869
      {
NEW
870
        logApplyConflict( "update_nothing", entry );
×
NEW
871
        context()->logger().warn( "Wrong number of affected rows! Expected 1, got: " + res.affectedRows() + "\nSQL: " + sql );
×
872
        return ChangeApplyResult::NoChange;
×
873
      }
874
    }
6✔
875
    else if ( entry.op == ChangesetEntry::OpDelete )
3✔
876
    {
877
      std::string sql = sqlForDelete( mBaseSchema, tableName, tbl.schema, entry.oldValues );
3✔
878
      PostgresResult res( execSql( mConn, sql ) );
3✔
879
      if ( res.affectedRows() != "1" )
2✔
880
      {
881
        logApplyConflict( "delete_nothing", entry );
×
NEW
882
        context()->logger().warn( "Wrong number of affected rows! Expected 1, got: " + res.affectedRows() );
×
NEW
883
        return ChangeApplyResult::NoChange;
×
884
      }
885
    }
3✔
886
    else
NEW
887
      throw GeoDiffException( "Unexpected operation" );
×
888
  }
889
  catch ( GeoDiffPostgresException &ex )
1✔
890
  {
891
    if ( isIntegrityError( ex.result() ) )
1✔
892
    {
NEW
893
      execSql( mConn, "ROLLBACK TO SAVEPOINT geodiff_apply" );
×
NEW
894
      return ChangeApplyResult::ConstraintConflict;
×
895
    }
896
    throw;
1✔
897
  }
1✔
898

899
  execSql( mConn, "RELEASE SAVEPOINT geodiff_apply" );
84✔
900
  return ChangeApplyResult::Applied;
84✔
901
}
86✔
902

903

904
void PostgresDriver::applyChangeset( ChangesetReader &reader )
24✔
905
{
906
  if ( !mConn )
24✔
NEW
907
    throw GeoDiffException( "Not connected to a database" );
×
908

909
  // start a transaction, so that all changes get committed at once (or nothing get committed)
910
  PostgresTransaction transaction( mConn );
24✔
911
  execSql( mConn, "SET CONSTRAINTS ALL DEFERRED" );
24✔
912

913
  // See sqlitedriver.cpp for why and how we're trying to apply changes
914
  // multiple times
915
  int unrecoverableConflictCount = 0;
24✔
916
  std::vector<ChangesetEntry> conflictingEntries;
24✔
917
  ChangesetEntry entry;
24✔
918
  PostgresChangeApplyState state;
24✔
919
  std::unordered_map<std::string, std::unique_ptr<ChangesetTable>> tableCopies;
24✔
920
  while ( reader.nextEntry( entry ) )
109✔
921
  {
922
    ChangeApplyResult res = applyChange( state, entry );
86✔
923
    switch ( res )
85✔
924
    {
925
      case ChangeApplyResult::Applied:
85✔
926
      case ChangeApplyResult::Skipped:
927
        break;
85✔
NEW
928
      case ChangeApplyResult::ConstraintConflict:
×
NEW
929
        if ( tableCopies.count( entry.table->name ) == 0 )
×
NEW
930
          tableCopies[entry.table->name] = std::unique_ptr<ChangesetTable>( new ChangesetTable( *entry.table ) );
×
NEW
931
        entry.table = tableCopies[entry.table->name].get();
×
NEW
932
        conflictingEntries.push_back( entry );
×
NEW
933
        break;
×
NEW
934
      case ChangeApplyResult::NoChange:
×
NEW
935
        unrecoverableConflictCount++;
×
NEW
936
        break;
×
937
    }
938
  }
939

940
  std::vector<ChangesetEntry> newConflictingEntries;
46✔
941
  while ( conflictingEntries.size() > 0 )
23✔
942
  {
NEW
943
    for ( const ChangesetEntry &centry : conflictingEntries )
×
944
    {
NEW
945
      ChangeApplyResult res = applyChange( state, centry );
×
NEW
946
      switch ( res )
×
947
      {
NEW
948
        case ChangeApplyResult::Applied:
×
949
        case ChangeApplyResult::Skipped:
NEW
950
          break;
×
NEW
951
        case ChangeApplyResult::ConstraintConflict:
×
NEW
952
          newConflictingEntries.push_back( centry );
×
NEW
953
          break;
×
NEW
954
        case ChangeApplyResult::NoChange:
×
NEW
955
          unrecoverableConflictCount++;
×
NEW
956
          break;
×
957
      }
958
    }
959

NEW
960
    if ( newConflictingEntries.size() == conflictingEntries.size() )
×
961
    {
NEW
962
      for ( const ChangesetEntry &centry : conflictingEntries )
×
963
        logApplyConflict( "unresolvable_conflict", centry );
×
964
      throw GeoDiffException( "Could not resolve dependencies in constraint conflicts." );
×
965
    }
NEW
966
    conflictingEntries = newConflictingEntries;
×
NEW
967
    newConflictingEntries.clear();
×
968
  }
969

970
  // at the end, update any SEQUENCE objects if needed
971
  for ( const auto &it : state.tableState )
66✔
972
    if ( it.second.autoIncrementMax )
43✔
973
      updateSequenceObject( it.second.sequenceName, it.second.autoIncrementMax );
40✔
974

975
  if ( !unrecoverableConflictCount )
23✔
976
  {
977
    transaction.commitChanges();
23✔
978
  }
979
  else
980
  {
981
    throw GeoDiffException( "Conflicts encountered while applying changes! Total " + std::to_string( unrecoverableConflictCount ) );
×
982
  }
983
}
28✔
984

985
std::string PostgresDriver::getSequenceObjectName( const TableSchema &tbl, int &autoIncrementPkeyIndex )
44✔
986
{
987
  std::string colName;
44✔
988
  autoIncrementPkeyIndex = -1;
44✔
989
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
44✔
990
  {
991
    if ( tbl.columns[i].isPrimaryKey && tbl.columns[i].isAutoIncrement )
44✔
992
    {
993
      autoIncrementPkeyIndex = i;
44✔
994
      colName = tbl.columns[i].name;
44✔
995
      break;
44✔
996
    }
997
  }
998

999
  if ( autoIncrementPkeyIndex == -1 )
44✔
1000
    return "";
×
1001

1002
  std::string tableNameString = quotedIdentifier( mBaseSchema ) + "." + quotedIdentifier( tbl.name );
44✔
1003
  std::string sql = "select pg_get_serial_sequence(" + quotedString( tableNameString ) + ", " + quotedString( colName ) + ")";
44✔
1004
  PostgresResult resBase( execSql( mConn, sql ) );
44✔
1005
  if ( resBase.rowCount() != 1 )
44✔
1006
    throw GeoDiffException( "Unable to find sequence object for auto-incrementing pkey for table " + tbl.name );
×
1007

1008
  return resBase.value( 0, 0 );
44✔
1009
}
44✔
1010

1011
void PostgresDriver::updateSequenceObject( const std::string &seqName, int64_t maxValue )
40✔
1012
{
1013
  PostgresResult resCurrVal( execSql( mConn, "SELECT last_value FROM " + seqName ) );
40✔
1014
  std::string currValueStr = resCurrVal.value( 0, 0 );
40✔
1015
  int currValue = std::stoi( currValueStr );
40✔
1016

1017
  if ( currValue < maxValue )
40✔
1018
  {
1019
    context()->logger().info( "Updating sequence " + seqName + " from " + std::to_string( currValue ) + " to " + std::to_string( maxValue ) );
20✔
1020

1021
    std::string sql = "SELECT setval(" + quotedString( seqName ) + ", " + std::to_string( maxValue ) + ")";
20✔
1022
    PostgresResult resSetVal( execSql( mConn, sql ) );
20✔
1023
    // the SQL just returns the new value we set
1024
  }
20✔
1025
}
40✔
1026

1027
void PostgresDriver::createTables( const std::vector<TableSchema> &tables )
16✔
1028
{
1029
  for ( const TableSchema &tbl : tables )
53✔
1030
  {
1031
    if ( startsWith( tbl.name, "gpkg_" ) )
74✔
1032
      continue;   // skip any changes to GPKG meta tables
×
1033

1034
    std::string sql, pkeyCols, columns;
37✔
1035
    for ( const TableColumnInfo &c : tbl.columns )
195✔
1036
    {
1037
      if ( !columns.empty() )
158✔
1038
        columns += ", ";
121✔
1039

1040
      std::string type = c.type.dbType;
158✔
1041
      if ( c.isAutoIncrement )
158✔
1042
        type = "SERIAL";   // there is also "smallserial", "bigserial" ...
37✔
1043
      columns += quotedIdentifier( c.name ) + " " + type;
158✔
1044

1045
      if ( c.isNotNull )
158✔
1046
        columns += " NOT NULL";
36✔
1047

1048
      if ( c.isPrimaryKey )
158✔
1049
      {
1050
        if ( !pkeyCols.empty() )
37✔
1051
          pkeyCols += ", ";
×
1052
        pkeyCols += quotedIdentifier( c.name );
37✔
1053
      }
1054
    }
158✔
1055

1056
    sql = "CREATE TABLE " + quotedIdentifier( mBaseSchema ) + "." + quotedIdentifier( tbl.name ) + " (";
37✔
1057
    sql += columns;
37✔
1058
    sql += ", PRIMARY KEY (" + pkeyCols + ")";
37✔
1059
    sql += ");";
37✔
1060

1061
    PostgresResult res( execSql( mConn, sql ) );
37✔
1062
    if ( res.status() != PGRES_COMMAND_OK )
37✔
1063
      throw GeoDiffException( "Failure creating table: " + res.statusErrorMessage() );
×
1064
  }
37✔
1065
}
16✔
1066

1067

1068
void PostgresDriver::dumpData( ChangesetWriter &writer, bool useModified )
15✔
1069
{
1070
  if ( !mConn )
15✔
1071
    throw GeoDiffException( "Not connected to a database" );
×
1072

1073
  std::vector<std::string> tables = listTables();
15✔
1074
  for ( const std::string &tableName : tables )
43✔
1075
  {
1076
    TableSchema tbl = tableSchema( tableName, useModified );
28✔
1077
    if ( !tbl.hasPrimaryKey() )
28✔
1078
      continue;  // ignore tables without primary key - they can't be compared properly
×
1079

1080
    std::string sql = "SELECT " + allColumnNames( tbl ) + " FROM " +
56✔
1081
                      quotedIdentifier( useModified ? mModifiedSchema : mBaseSchema ) + "." + quotedIdentifier( tableName );
84✔
1082

1083
    PostgresResult res( execSql( mConn, sql ) );
28✔
1084
    int rows = res.rowCount();
28✔
1085
    for ( int r = 0; r < rows; ++r )
83✔
1086
    {
1087
      if ( r == 0 )
55✔
1088
      {
1089
        writer.beginTable( schemaToChangesetTable( tableName, tbl ) );
28✔
1090
      }
1091

1092
      ChangesetEntry e;
55✔
1093
      e.op = ChangesetEntry::OpInsert;
55✔
1094
      size_t numColumns = tbl.columns.size();
55✔
1095
      for ( size_t i = 0; i < numColumns; ++i )
268✔
1096
      {
1097
        e.newValues.push_back( Value( resultToValue( res, r, i, tbl.columns[i] ) ) );
213✔
1098
      }
1099
      writer.writeEntry( e );
55✔
1100
    }
55✔
1101
  }
28✔
1102
}
15✔
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