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

MerginMaps / geodiff / 29455580701

15 Jul 2026 10:29PM UTC coverage: 87.279% (-0.3%) from 87.592%
29455580701

Pull #252

github

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

1112 of 1244 new or added lines in 13 files covered. (89.39%)

13 existing lines in 3 files now uncovered.

4336 of 4968 relevant lines covered (87.28%)

623.66 hits per line

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

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

6
#include "sqlitedriver.h"
7

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

20
#include <memory.h>
21
#include <sqlite3.h>
22
#include <unordered_map>
23
#include <variant>
24

25

26
void SqliteDriver::logApplyConflict( const std::string &type, const ChangesetEntry &entry, bool isDbErr ) const
5✔
27
{
28
  std::string msg = "CONFLICT: " + type;
5✔
29
  if ( isDbErr )
5✔
30
    msg += " (" + std::string( sqlite3_errmsg( mDb->get() ) ) + ")";
×
31
  msg += ":\n" + changesetEntryToJSON( entry ).dump( 2 );
5✔
32
  context()->logger().warn( msg );
5✔
33
}
5✔
34

35
/**
36
 * Wrapper around SQLite database wide mutex.
37
 */
38
class Sqlite3DbMutexLocker
39
{
40
  public:
41
    explicit Sqlite3DbMutexLocker( std::shared_ptr<Sqlite3Db> db )
129✔
42
      : mDb( db )
129✔
43
    {
44
      sqlite3_mutex_enter( sqlite3_db_mutex( mDb.get()->get() ) );
129✔
45
    }
129✔
46
    ~Sqlite3DbMutexLocker()
129✔
47
    {
48
      sqlite3_mutex_leave( sqlite3_db_mutex( mDb.get()->get() ) );
129✔
49
    }
129✔
50

51
  private:
52
    std::shared_ptr<Sqlite3Db> mDb;
53
};
54

55
/**
56
 * Wrapper around SQLite Savepoint Transactions.
57
 *
58
 * Constructor start a trasaction, it needs to be confirmed by a call to commitChanges() when
59
 * changes are ready to be written. If commitChanges() is not called, changes since the constructor
60
 * will be rolled back (so that on exception everything gets cleaned up properly).
61
 */
62
class Sqlite3SavepointTransaction
63
{
64
  public:
65
    explicit Sqlite3SavepointTransaction( const Context *context, std::shared_ptr<Sqlite3Db> db )
132✔
66
      : mDb( db ), mContext( context )
132✔
67
    {
68
      if ( sqlite3_exec( mDb.get()->get(), "SAVEPOINT changeset_apply", 0, 0, 0 ) != SQLITE_OK )
132✔
69
      {
70
        throwSqliteError( mDb.get()->get(), "Unable to start savepoint transaction" );
×
71
      }
72
    }
132✔
73

74
    ~Sqlite3SavepointTransaction()
132✔
75
    {
76
      if ( mDb )
132✔
77
      {
78
        // we had some problems - roll back any pending changes
79
        if ( sqlite3_exec( mDb.get()->get(), "ROLLBACK TO changeset_apply", 0, 0, 0 ) != SQLITE_OK )
8✔
80
        {
81
          logSqliteError( mContext, mDb, "Unable to rollback savepoint transaction" );
×
82
        }
83
        if ( sqlite3_exec( mDb.get()->get(), "RELEASE changeset_apply", 0, 0, 0 ) != SQLITE_OK )
8✔
84
        {
85
          logSqliteError( mContext, mDb, "Unable to release savepoint" );
×
86
        }
87
      }
88
    }
132✔
89

90
    void commitChanges()
126✔
91
    {
92
      assert( mDb );
126✔
93
      // there were no errors - release the savepoint and our changes get saved
94
      if ( sqlite3_exec( mDb.get()->get(), "RELEASE changeset_apply", 0, 0, 0 ) != SQLITE_OK )
126✔
95
      {
96
        throwSqliteError( mDb.get()->get(), "Failed to release savepoint" );
4✔
97
      }
98
      // reset handler to the database so that the destructor does nothing
99
      mDb.reset();
124✔
100
    }
124✔
101

102
  private:
103
    std::shared_ptr<Sqlite3Db> mDb;
104
    const Context *mContext;
105
};
106

107

108
///////
109

110

111
SqliteDriver::SqliteDriver( const Context *context )
611✔
112
  : Driver( context )
611✔
113
{
114
}
611✔
115

116
// Opens 'base' DB (with implicit schema called 'main') and optionally
117
// 'modified' DB (with explicit schema 'modified')
118
void SqliteDriver::open( const DriverParametersMap &conn )
533✔
119
{
120
  DriverParametersMap::const_iterator connBaseIt = conn.find( "base" );
533✔
121
  if ( connBaseIt == conn.end() )
533✔
122
    throw GeoDiffException( "Missing 'base' file" );
3✔
123

124
  DriverParametersMap::const_iterator connModifiedIt = conn.find( "modified" );
532✔
125
  mHasModified = connModifiedIt != conn.end();
532✔
126

127
  std::string base = connBaseIt->second;
532✔
128
  if ( !fileexists( base ) )
532✔
129
  {
130
    throw GeoDiffException( "Missing 'base' file when opening sqlite driver: " + base );
6✔
131
  }
132

133
  mDb = std::make_shared<Sqlite3Db>();
526✔
134
  mDb->open( base );
526✔
135

136
  if ( mHasModified )
526✔
137
  {
138
    std::string modified = connModifiedIt->second;
314✔
139

140
    if ( !fileexists( modified ) )
314✔
141
    {
142
      throw GeoDiffException( "Missing 'modified' file when opening sqlite driver: " + modified );
2✔
143
    }
144

145
    {
146
      Buffer sqlBuf;
312✔
147
      sqlBuf.printf( "ATTACH '%q' AS modified", modified.c_str() );
312✔
148
      mDb->exec( sqlBuf );
312✔
149
    }
312✔
150
  }
314✔
151

152
  // GeoPackage triggers require few functions like ST_IsEmpty() to be registered
153
  // in order to be able to apply changesets
154
  if ( isGeoPackage( context(), mDb ) )
523✔
155
  {
156
    register_gpkg_extensions( mDb );
444✔
157
  }
158

159
  // Enable foreign key constraints (if the database has any)
160
  Buffer sqlBuf;
523✔
161
  sqlBuf.printf( "PRAGMA foreign_keys = 1" );
523✔
162
  mDb->exec( sqlBuf );
523✔
163
}
532✔
164

165
void SqliteDriver::create( const DriverParametersMap &conn, bool overwrite )
77✔
166
{
167
  DriverParametersMap::const_iterator connBaseIt = conn.find( "base" );
77✔
168
  if ( connBaseIt == conn.end() )
77✔
169
    throw GeoDiffException( "Missing 'base' file" );
×
170

171
  std::string base = connBaseIt->second;
77✔
172

173
  if ( overwrite )
77✔
174
  {
175
    fileremove( base );  // remove if the file exists already
77✔
176
  }
177

178
  mDb = std::make_shared<Sqlite3Db>();
77✔
179
  mDb->create( base );
77✔
180

181
  // register geopackage related functions in the newly created sqlite database
182
  register_gpkg_extensions( mDb );
77✔
183
}
77✔
184

185
std::string SqliteDriver::databaseName( bool useModified )
1,956✔
186
{
187
  if ( mHasModified )
1,956✔
188
  {
189
    return useModified ? "modified" : "main";
2,970✔
190
  }
191
  else
192
  {
193
    if ( useModified )
471✔
194
      throw GeoDiffException( "'modified' table not open" );
×
195
    return "main";
942✔
196
  }
197
}
198

199
std::vector<std::string> SqliteDriver::listTables( bool useModified )
726✔
200
{
201
  std::string dbName = databaseName( useModified );
726✔
202
  std::vector<std::string> tableNames;
726✔
203
  std::string all_tables_sql = "SELECT name FROM " + dbName + ".sqlite_master\n"
1,452✔
204
                               " WHERE type='table' AND sql NOT LIKE 'CREATE VIRTUAL%%'\n"
205
                               " ORDER BY name";
726✔
206
  Sqlite3Stmt statement;
726✔
207
  statement.prepare( mDb, "%s", all_tables_sql.c_str() );
726✔
208
  int rc;
209
  while ( SQLITE_ROW == ( rc = sqlite3_step( statement.get() ) ) )
9,169✔
210
  {
211
    const char *name = reinterpret_cast<const char *>( sqlite3_column_text( statement.get(), 0 ) );
8,443✔
212
    if ( !name )
8,443✔
213
      continue;
7,409✔
214

215
    std::string tableName( name );
16,886✔
216
    /* typically geopackage from ogr would have these (table name is simple)
217
    gpkg_contents
218
    gpkg_extensions
219
    gpkg_geometry_columns
220
    gpkg_ogr_contents
221
    gpkg_spatial_ref_sys
222
    gpkg_tile_matrix
223
    gpkg_tile_matrix_set
224
    rtree_simple_geometry_node
225
    rtree_simple_geometry_parent
226
    rtree_simple_geometry_rowid
227
    simple (or any other name(s) of layers)
228
    sqlite_sequence
229
    */
230

231
    // table handled by triggers trigger_*_feature_count_*
232
    if ( startsWith( tableName, "gpkg_" ) )
16,886✔
233
      continue;
4,718✔
234
    // table handled by triggers rtree_*_geometry_*
235
    if ( startsWith( tableName, "rtree_" ) )
7,450✔
236
      continue;
2,040✔
237
    // internal table for AUTOINCREMENT
238
    if ( tableName == "sqlite_sequence" )
1,685✔
239
      continue;
622✔
240

241
    if ( context()->isTableSkipped( tableName ) )
1,063✔
242
      continue;
29✔
243

244
    tableNames.push_back( tableName );
1,034✔
245
  }
8,443✔
246
  if ( rc != SQLITE_DONE )
726✔
247
  {
248
    logSqliteError( context(), mDb, "Failed to list SQLite tables" );
×
249
  }
250

251
  // result is ordered by name
252
  return tableNames;
1,452✔
253
}
726✔
254

255
bool tableExists( std::shared_ptr<Sqlite3Db> db, const std::string &tableName, const std::string &dbName )
2,390✔
256
{
257
  Sqlite3Stmt stmtHasGeomColumnsInfo;
2,390✔
258
  stmtHasGeomColumnsInfo.prepare( db, "SELECT name FROM \"%w\".sqlite_master WHERE type='table' "
2,390✔
259
                                  "AND name='%q'", dbName.c_str(), tableName.c_str() );
260
  return sqlite3_step( stmtHasGeomColumnsInfo.get() ) == SQLITE_ROW;
4,780✔
261
}
2,390✔
262

263
TableSchema SqliteDriver::tableSchema( const std::string &tableName,
1,196✔
264
                                       bool useModified )
265
{
266
  std::string dbName = databaseName( useModified );
1,196✔
267

268
  if ( !tableExists( mDb, tableName, dbName ) )
1,196✔
269
    throw GeoDiffException( "Table does not exist: " + tableName );
2✔
270

271
  TableSchema tbl;
1,194✔
272
  tbl.name = tableName;
1,194✔
273
  std::map<std::string, std::string> columnTypes;
1,194✔
274

275
  Sqlite3Stmt statement;
1,194✔
276
  statement.prepare( mDb, "PRAGMA '%q'.table_info('%q')", dbName.c_str(), tableName.c_str() );
1,194✔
277
  int rc;
278
  while ( SQLITE_ROW == ( rc = sqlite3_step( statement.get() ) ) )
5,501✔
279
  {
280
    const unsigned char *zName = sqlite3_column_text( statement.get(), 1 );
4,307✔
281
    if ( zName == nullptr )
4,307✔
282
      throw GeoDiffException( "NULL column name in table schema: " + tableName );
×
283

284
    TableColumnInfo columnInfo;
4,307✔
285
    columnInfo.name = reinterpret_cast<const char *>( zName );
4,307✔
286
    columnInfo.isNotNull = sqlite3_column_int( statement.get(), 3 );
4,307✔
287
    columnInfo.isPrimaryKey = sqlite3_column_int( statement.get(), 5 );
4,307✔
288
    columnTypes[columnInfo.name] = reinterpret_cast<const char *>( sqlite3_column_text( statement.get(), 2 ) );
4,307✔
289

290
    tbl.columns.push_back( columnInfo );
4,307✔
291
  }
4,307✔
292
  if ( rc != SQLITE_DONE )
1,194✔
293
  {
294
    logSqliteError( context(), mDb, "Failed to get list columns for table " + tableName );
×
295
  }
296

297
  // check if the geometry columns table is present (it may not be if this is a "pure" sqlite file)
298
  if ( tableExists( mDb, "gpkg_geometry_columns", dbName ) )
2,388✔
299
  {
300
    //
301
    // get geometry column details (geometry type, whether it has Z/M values, CRS id)
302
    //
303

304
    int srsId = -1;
976✔
305
    Sqlite3Stmt stmtGeomCol;
976✔
306
    stmtGeomCol.prepare( mDb, "SELECT * FROM \"%w\".gpkg_geometry_columns WHERE table_name = '%q'", dbName.c_str(), tableName.c_str() );
976✔
307
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmtGeomCol.get() ) ) )
1,863✔
308
    {
309
      const unsigned char *chrColumnName = sqlite3_column_text( stmtGeomCol.get(), 1 );
887✔
310
      const unsigned char *chrTypeName = sqlite3_column_text( stmtGeomCol.get(), 2 );
887✔
311
      if ( chrColumnName == nullptr )
887✔
312
        throw GeoDiffException( "NULL column name in gpkg_geometry_columns: " + tableName );
×
313
      if ( chrTypeName == nullptr )
887✔
314
        throw GeoDiffException( "NULL type name in gpkg_geometry_columns: " + tableName );
×
315

316
      std::string geomColName = reinterpret_cast<const char *>( chrColumnName );
1,774✔
317
      std::string geomTypeName = reinterpret_cast<const char *>( chrTypeName );
887✔
318
      srsId = sqlite3_column_int( stmtGeomCol.get(), 3 );
887✔
319
      bool hasZ = sqlite3_column_int( stmtGeomCol.get(), 4 );
887✔
320
      bool hasM = sqlite3_column_int( stmtGeomCol.get(), 5 );
887✔
321

322
      size_t i = tbl.columnFromName( geomColName );
887✔
323
      if ( i == SIZE_MAX )
887✔
324
        throw GeoDiffException( "Inconsistent entry in gpkg_geometry_columns - geometry column not found: " + geomColName );
×
325

326
      TableColumnInfo &col = tbl.columns[i];
887✔
327
      col.setGeometry( geomTypeName, srsId, hasM, hasZ );
887✔
328
    }
887✔
329
    if ( rc != SQLITE_DONE )
976✔
330
    {
331
      logSqliteError( context(), mDb, "Failed to get geometry column info for table " + tableName );
×
332
    }
333

334
    //
335
    // get CRS information
336
    //
337

338
    if ( srsId != -1 )
976✔
339
    {
340
      Sqlite3Stmt stmtCrs;
768✔
341
      stmtCrs.prepare( mDb, "SELECT * FROM \"%w\".gpkg_spatial_ref_sys WHERE srs_id = %d", dbName.c_str(), srsId );
768✔
342
      if ( SQLITE_ROW != sqlite3_step( stmtCrs.get() ) )
768✔
343
      {
344
        throwSqliteError( mDb->get(), "Unable to find entry in gpkg_spatial_ref_sys for srs_id = " + std::to_string( srsId ) );
×
345
      }
346

347
      const unsigned char *chrAuthName = sqlite3_column_text( stmtCrs.get(), 2 );
768✔
348
      const unsigned char *chrWkt = sqlite3_column_text( stmtCrs.get(), 4 );
768✔
349
      if ( chrAuthName == nullptr )
768✔
350
        throw GeoDiffException( "NULL auth name in gpkg_spatial_ref_sys: " + tableName );
×
351
      if ( chrWkt == nullptr )
768✔
352
        throw GeoDiffException( "NULL definition in gpkg_spatial_ref_sys: " + tableName );
×
353

354
      tbl.crs.srsId = srsId;
768✔
355
      tbl.crs.authName = reinterpret_cast<const char *>( chrAuthName );
768✔
356
      tbl.crs.authCode = sqlite3_column_int( stmtCrs.get(), 3 );
768✔
357
      tbl.crs.wkt = reinterpret_cast<const char *>( chrWkt );
768✔
358
    }
768✔
359
  }
976✔
360

361
  // update column types
362
  for ( auto const &it : columnTypes )
5,501✔
363
  {
364
    size_t i = tbl.columnFromName( it.first );
4,307✔
365
    TableColumnInfo &col = tbl.columns[i];
4,307✔
366
    tbl.columns[i].type = columnType( context(), it.second, Driver::SQLITEDRIVERNAME, col.isGeometry );
4,307✔
367

368
    if ( col.isPrimaryKey && ( lowercaseString( col.type.dbType ) == "integer" ) )
4,307✔
369
    {
370
      // sqlite uses auto-increment automatically for INTEGER PRIMARY KEY - https://sqlite.org/autoinc.html
371
      col.isAutoIncrement = true;
1,180✔
372
    }
373
  }
374

375
  return tbl;
2,388✔
376
}
1,196✔
377

378
DatabaseSchema SqliteDriver::getSchema( bool useModified )
620✔
379
{
380
  std::vector<TableSchema> tables;
620✔
381
  for ( const std::string &name : listTables( useModified ) )
1,482✔
382
  {
383
    tables.push_back( tableSchema( name, useModified ) );
862✔
384
  }
620✔
385
  return {tables};
1,240✔
386
}
620✔
387

388
/**
389
 * printf() with sqlite extensions - see https://www.sqlite.org/printf.html
390
 * for extra format options like %q or %Q
391
 */
392
static std::string sqlitePrintf( const char *zFormat, ... )
12,780✔
393
{
394
  va_list ap;
395
  va_start( ap, zFormat );
12,780✔
396
  char *zSql = sqlite3_vmprintf( zFormat, ap );
12,780✔
397
  va_end( ap );
12,780✔
398

399
  if ( zSql == nullptr )
12,780✔
400
  {
401
    throw GeoDiffException( "out of memory" );
×
402
  }
403
  std::string res = reinterpret_cast<const char *>( zSql );
12,780✔
404
  sqlite3_free( zSql );
12,780✔
405
  return res;
25,560✔
406
}
×
407

408
struct TableDiffContext
409
{
410
  std::shared_ptr<Sqlite3Db> db;
411
  const TableSchema &schemaBase;
412
  const TableSchema &schemaModified;
413
  std::vector<TableColumnInfo> commonColumns;
414
  std::vector<TableColumnInfo> newColumns;
415
  ChangesetWriter &writer;
416
  bool tableEntryWritten = false;
417
};
418

419
static std::string sqlColumnsStr( const TableDiffContext &diffContext, bool reverse )
1,676✔
420
{
421
  const char *tableName = ( reverse ? diffContext.schemaBase.name : diffContext.schemaModified.name ).c_str();
1,676✔
422

423
  std::string colsStr; // Column list equivalent to modified schema
1,676✔
424
  for ( const TableColumnInfo &c : diffContext.schemaModified.columns )
7,720✔
425
  {
426
    if ( !colsStr.empty() )
6,044✔
427
      colsStr += ", ";
4,368✔
428
    if ( reverse )
6,044✔
429
    {
430
      // Check if this column also exists in base and NULL it out if not
431
      bool found = false;
3,022✔
432
      for ( const auto &commonCol : diffContext.commonColumns )
7,182✔
433
      {
434
        if ( commonCol.name == c.name )
7,160✔
435
        {
436
          found = true;
3,000✔
437
          break;
3,000✔
438
        }
439
      }
440
      if ( !found )
3,022✔
441
      {
442
        colsStr += sqlitePrintf( "NULL AS \"%w\"", c.name.c_str() );
22✔
443
        continue;
22✔
444
      }
445
    }
446
    colsStr += sqlitePrintf( "\"%w\".\"%w\".\"%w\"",
12,044✔
447
                             reverse ? "main" : "modified", tableName, c.name.c_str() );
6,022✔
448
  }
449
  return colsStr;
1,676✔
NEW
450
}
×
451

452
//! Constructs SQL query to get all rows that do not exist in the other table (used for insert and delete)
453
static std::string sqlFindInserted( const TableDiffContext &diffContext, bool reverse )
838✔
454
{
455
  const char *baseTableName = diffContext.schemaBase.name.c_str();
838✔
456
  const char *modifiedTableName = diffContext.schemaModified.name.c_str();
838✔
457

458
  std::string exprPk; // Filter expression checking primary key is equal
838✔
459
  for ( const TableColumnInfo &c : diffContext.commonColumns )
3,838✔
460
  {
461
    if ( c.isPrimaryKey )
3,000✔
462
    {
463
      if ( !exprPk.empty() )
846✔
464
        exprPk += " AND ";
8✔
465
      exprPk += sqlitePrintf( "\"modified\".\"%w\".\"%w\"=\"main\".\"%w\".\"%w\"",
1,692✔
466
                              modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
846✔
467
    }
468
  }
469

470
  std::string sql = sqlitePrintf( "SELECT %s FROM \"%w\".\"%w\" WHERE NOT EXISTS ( SELECT 1 FROM \"%w\".\"%w\" WHERE %s)",
471
                                  sqlColumnsStr( diffContext, reverse ).c_str(),
1,676✔
472
                                  reverse ? "main" : "modified", reverse ? baseTableName : modifiedTableName,
473
                                  reverse ? "modified" : "main", reverse ? modifiedTableName : baseTableName, exprPk.c_str() );
1,676✔
474
  return sql;
1,676✔
475
}
838✔
476

477
//! Constructs SQL query to get all modified rows for a single table
478
static std::string sqlFindModified( const TableDiffContext &diffContext )
419✔
479
{
480
  const char *baseTableName = diffContext.schemaBase.name.c_str();
419✔
481
  const char *modifiedTableName = diffContext.schemaModified.name.c_str();
419✔
482

483
  std::string exprPk;
419✔
484
  std::string exprOther;
419✔
485
  for ( const TableColumnInfo &c : diffContext.commonColumns )
1,919✔
486
  {
487
    if ( c.isPrimaryKey )
1,500✔
488
    {
489
      if ( !exprPk.empty() )
423✔
490
        exprPk += " AND ";
4✔
491
      exprPk += sqlitePrintf( "\"modified\".\"%w\".\"%w\"=\"main\".\"%w\".\"%w\"",
846✔
492
                              modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
423✔
493
    }
494
    else // not a primary key column
495
    {
496
      if ( !exprOther.empty() )
1,077✔
497
        exprOther += " OR ";
659✔
498

499
      exprOther += sqlitePrintf( "\"modified\".\"%w\".\"%w\" IS NOT \"main\".\"%w\".\"%w\"",
2,154✔
500
                                 modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
1,077✔
501
    }
502
  }
503

504
  // Check for non-NULL values in newly-added columns
505
  for ( const TableColumnInfo &c : diffContext.newColumns )
430✔
506
  {
507
    if ( !exprOther.empty() )
11✔
508
      exprOther += " OR ";
11✔
509

510
    exprOther += sqlitePrintf( "\"modified\".\"%w\".\"%w\" IS NOT NULL",
22✔
511
                               modifiedTableName, c.name.c_str() );
11✔
512
  }
513

514
  std::string colsStr = sqlColumnsStr( diffContext, false ) + ", " + sqlColumnsStr( diffContext, true );
419✔
515

516
  if ( exprOther.empty() )
419✔
517
  {
518
    return sqlitePrintf( "SELECT %s FROM \"modified\".\"%w\", \"main\".\"%w\" WHERE %s",
519
                         colsStr.c_str(), modifiedTableName, baseTableName, exprPk.c_str() );
1✔
520
  }
521
  else
522
  {
523
    return sqlitePrintf( "SELECT %s FROM \"modified\".\"%w\", \"main\".\"%w\" WHERE %s AND (%s)",
524
                         colsStr.c_str(), modifiedTableName, baseTableName, exprPk.c_str(), exprOther.c_str() );
418✔
525
  }
526
}
419✔
527

528

529
static Value changesetValue( sqlite3_value *v )
1,702✔
530
{
531
  Value x;
1,702✔
532
  int type = sqlite3_value_type( v );
1,702✔
533
  if ( type == SQLITE_NULL )
1,702✔
534
    x.setNull();
154✔
535
  else if ( type == SQLITE_INTEGER )
1,548✔
536
    x.setInt( sqlite3_value_int64( v ) );
778✔
537
  else if ( type == SQLITE_FLOAT )
770✔
538
    x.setDouble( sqlite3_value_double( v ) );
15✔
539
  else if ( type == SQLITE_TEXT )
755✔
540
    x.setString( Value::TypeText, reinterpret_cast<const char *>( sqlite3_value_text( v ) ), sqlite3_value_bytes( v ) );
480✔
541
  else if ( type == SQLITE_BLOB )
275✔
542
    x.setString( Value::TypeBlob, reinterpret_cast<const char *>( sqlite3_value_blob( v ) ), sqlite3_value_bytes( v ) );
275✔
543
  else
544
    throw GeoDiffException( "Unexpected value type" );
×
545

546
  return x;
1,702✔
547
}
×
548

549
static void handleInserted( const Context *context, TableDiffContext &diffContext, bool reverse )
838✔
550
{
551
  std::string sqlInserted = sqlFindInserted( diffContext, reverse );
838✔
552
  Sqlite3Stmt statementI;
838✔
553
  statementI.prepare( diffContext.db, "%s", sqlInserted.c_str() );
838✔
554
  int rc;
555
  while ( SQLITE_ROW == ( rc = sqlite3_step( statementI.get() ) ) )
1,146✔
556
  {
557
    if ( !diffContext.tableEntryWritten )
308✔
558
    {
559
      ChangesetTable chTable = schemaToChangesetTable( diffContext.schemaModified.name, diffContext.schemaModified );
179✔
560
      diffContext.writer.beginTable( chTable );
179✔
561
      diffContext.tableEntryWritten = true;
179✔
562
    }
179✔
563

564
    ChangesetDataEntry e;
308✔
565
    e.op = reverse ? ChangesetDataEntry::OpDelete : ChangesetDataEntry::OpInsert;
308✔
566

567
    size_t numColumns = diffContext.schemaModified.columns.size();
308✔
568
    for ( size_t i = 0; i < numColumns; ++i )
1,392✔
569
    {
570
      Sqlite3Value v( sqlite3_column_value( statementI.get(), static_cast<int>( i ) ) );
1,084✔
571
      if ( reverse )
1,084✔
572
        e.oldValues.push_back( changesetValue( v.value() ) );
149✔
573
      else
574
        e.newValues.push_back( changesetValue( v.value() ) );
935✔
575
    }
1,084✔
576

577
    diffContext.writer.writeEntry( e );
308✔
578
  }
308✔
579
  if ( rc != SQLITE_DONE )
838✔
580
  {
NEW
581
    logSqliteError( context, diffContext.db, "Failed to write information about inserted rows in table " + diffContext.schemaModified.name );
×
582
  }
583
}
838✔
584

585
static void handleUpdated( const Context *context, TableDiffContext &diffContext )
419✔
586
{
587
  std::string sqlModified = sqlFindModified( diffContext );
419✔
588

589
  Sqlite3Stmt statement;
419✔
590
  statement.prepare( diffContext.db, "%s", sqlModified.c_str() );
419✔
591
  int rc;
592
  while ( SQLITE_ROW == ( rc = sqlite3_step( statement.get() ) ) )
505✔
593
  {
594
    /*
595
    ** Within the old.* record associated with an UPDATE change, all fields
596
    ** associated with table columns that are not PRIMARY KEY columns and are
597
    ** not modified by the UPDATE change are set to "undefined". Other fields
598
    ** are set to the values that made up the row before the UPDATE that the
599
    ** change records took place. Within the new.* record, fields associated
600
    ** with table columns modified by the UPDATE change contain the new
601
    ** values. Fields associated with table columns that are not modified
602
    ** are set to "undefined".
603
    */
604

605
    ChangesetDataEntry e;
86✔
606
    e.op = ChangesetDataEntry::OpUpdate;
86✔
607

608
    bool hasUpdates = false;
86✔
609
    size_t numColumns = diffContext.schemaModified.columns.size();
86✔
610
    for ( size_t i = 0; i < numColumns; ++i )
418✔
611
    {
612
      Sqlite3Value v1( sqlite3_column_value( statement.get(), static_cast<int>( i + numColumns ) ) );
332✔
613
      Sqlite3Value v2( sqlite3_column_value( statement.get(), static_cast<int>( i ) ) );
332✔
614
      bool pkey = diffContext.schemaModified.columns[i].isPrimaryKey;
332✔
615
      bool updated = ( v1 != v2 );
332✔
616
      if ( updated )
332✔
617
      {
618
        // Let's do a secondary check for some column types to avoid false positives, for example
619
        // multiple different string representations could be used for a single datetime value,
620
        // see "Time Values" section in https://sqlite.org/lang_datefunc.html
621
        // Use strftime() to take into account fractional seconds
622
        if ( diffContext.schemaModified.columns[i].type == TableColumnType::DATETIME )
99✔
623
        {
624
          Sqlite3Stmt stmtDatetime;
8✔
625
          stmtDatetime.prepare( diffContext.db, "SELECT STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?1) IS NOT STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?2)" );
8✔
626
          sqlite3_bind_value( stmtDatetime.get(), 1, v1.value() );
8✔
627
          sqlite3_bind_value( stmtDatetime.get(), 2, v2.value() );
8✔
628
          int res = sqlite3_step( stmtDatetime.get() );
8✔
629
          if ( SQLITE_ROW == res )
8✔
630
          {
631
            updated = sqlite3_column_int( stmtDatetime.get(), 0 );
8✔
632
          }
633
          else if ( SQLITE_DONE != res )
×
634
          {
NEW
635
            logSqliteError( context, diffContext.db, "Failed to write information about updated rows in table " + diffContext.schemaModified.name );
×
636
          }
637
        }
8✔
638

639
        if ( updated )
99✔
640
        {
641
          hasUpdates = true;
97✔
642
        }
643
      }
644
      e.oldValues.push_back( ( pkey || updated ) ? changesetValue( v1.value() ) : Value() );
515✔
645
      e.newValues.push_back( updated ? changesetValue( v2.value() ) : Value() );
332✔
646
    }
332✔
647

648
    if ( hasUpdates )
86✔
649
    {
650
      if ( !diffContext.tableEntryWritten )
84✔
651
      {
652
        ChangesetTable chTable = schemaToChangesetTable( diffContext.schemaModified.name, diffContext.schemaModified );
43✔
653
        diffContext.writer.beginTable( chTable );
43✔
654
        diffContext.tableEntryWritten = true;
43✔
655
      }
43✔
656

657
      diffContext.writer.writeEntry( e );
84✔
658
    }
659
  }
86✔
660
  if ( rc != SQLITE_DONE )
419✔
661
  {
NEW
662
    logSqliteError( context, diffContext.db, "Failed to write information about inserted rows in table " + diffContext.schemaModified.name );
×
663
  }
664
}
419✔
665

666
// To allow diff inversion to work, we first delete all rows when dropping a
667
// table, and NULL out all rows when dropping a column.
668
static void writeDataChangesForSchemaChange( std::shared_ptr<Sqlite3Db> db, const std::unordered_map<std::string, TableSchema> &currentSchemata, ChangesetWriter &writer, const ChangesetEntry &entry )
30✔
669
{
670
  if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
30✔
671
  {
672
    auto it = currentSchemata.find( dcEntry->tableName );
7✔
673
    if ( it == currentSchemata.end() )
7✔
NEW
674
      throw GeoDiffException( "Missing schema for table " + dcEntry->tableName );
×
675
    const TableSchema &table = it->second;
7✔
676

677
    size_t droppedColIdx = table.columnFromName( dcEntry->column.name );
7✔
678
    if ( droppedColIdx == SIZE_MAX )
7✔
NEW
679
      throw GeoDiffException( "Could not find column " + dcEntry->column.name + " to delete" );
×
680

681
    std::string pkeyColStr;
7✔
682
    for ( const TableColumnInfo &c : table.columns )
32✔
683
    {
684
      if ( c.isPrimaryKey )
25✔
685
      {
686
        if ( !pkeyColStr.empty() )
7✔
NEW
687
          pkeyColStr += ", ";
×
688
        pkeyColStr += sqlitePrintf( "\"%w\"", c.name.c_str() );
7✔
689
      }
690
    }
691
    if ( pkeyColStr.empty() )
7✔
NEW
692
      throw GeoDiffException( "Table " + table.name + " has no primary key" );
×
693

694
    Sqlite3Stmt stmt;
7✔
695
    stmt.prepare( db, "SELECT %s, \"%w\" FROM \"main\".\"%w\" WHERE \"%w\" IS NOT NULL",
7✔
696
                  pkeyColStr.c_str(), dcEntry->column.name.c_str(), dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
697

698
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
7✔
699
    int rc;
700
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
28✔
701
    {
702
      ChangesetDataEntry e;
21✔
703
      e.op = ChangesetDataEntry::OpUpdate;
21✔
704

705
      size_t idxInResult = 0;
21✔
706
      for ( size_t i = 0; i < table.columns.size(); ++i )
96✔
707
      {
708
        bool isPkey = table.columns[i].isPrimaryKey;
75✔
709
        bool isDroppedCol = i == droppedColIdx;
75✔
710

711
        if ( isPkey || isDroppedCol )
75✔
712
        {
713
          Sqlite3Value v( sqlite3_column_value( stmt.get(), static_cast<int>( idxInResult ) ) );
42✔
714
          e.oldValues.push_back( changesetValue( v.value() ) );
42✔
715
          idxInResult++;
42✔
716
        }
42✔
717
        else
718
          e.oldValues.push_back( Value() );
33✔
719

720
        if ( isDroppedCol )
75✔
721
        {
722
          Value nullVal;
21✔
723
          nullVal.setNull();
21✔
724
          e.newValues.push_back( nullVal );
21✔
725
        }
21✔
726
        else
727
          e.newValues.push_back( Value() );
54✔
728
      }
729

730
      writer.writeEntry( e );
21✔
731
    }
21✔
732
  }
7✔
733
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
23✔
734
  {
735
    auto it = currentSchemata.find( dtEntry->tableName );
5✔
736
    if ( it == currentSchemata.end() )
5✔
NEW
737
      throw GeoDiffException( "Missing schema for table " + dtEntry->tableName );
×
738
    const TableSchema &table = it->second;
5✔
739

740
    Sqlite3Stmt stmt;
5✔
741
    stmt.prepare( db, "SELECT * FROM \"main\".\"%w\"", dtEntry->tableName.c_str() );
5✔
742

743
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
5✔
744
    int rc;
745
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
16✔
746
    {
747
      ChangesetDataEntry e;
11✔
748
      e.op = ChangesetDataEntry::OpDelete;
11✔
749

750
      size_t numColumns = table.columns.size();
11✔
751
      for ( size_t i = 0; i < numColumns; ++i )
44✔
752
      {
753
        Sqlite3Value v( sqlite3_column_value( stmt.get(), static_cast<int>( i ) ) );
33✔
754
        e.oldValues.push_back( changesetValue( v.value() ) );
33✔
755
      }
33✔
756

757
      writer.writeEntry( e );
11✔
758
    }
11✔
759
  }
5✔
760
}
30✔
761

762
void SqliteDriver::createChangeset( ChangesetWriter &writer )
310✔
763
{
764
  DatabaseSchema schemaBase = getSchema( false );
310✔
765
  DatabaseSchema schemaModified = getSchema( true );
310✔
766

767
  // We keep table schemata that have exactly the written out schema-change
768
  // entries applied. They're necessary to know the intermediate database state
769
  // for any data changes (e.g. row deletions before table drop).
770
  std::unordered_map<std::string, TableSchema> currentSchemata;
310✔
771
  for ( const TableSchema &tbl : schemaBase.tables )
740✔
772
    currentSchemata[tbl.name] = tbl;
430✔
773

774
  auto schemaDiffEntries = diffDatabaseSchema( schemaBase, schemaModified );
310✔
775
  for ( const ChangesetEntry &entry : schemaDiffEntries )
337✔
776
  {
777
    writeDataChangesForSchemaChange( mDb, currentSchemata, writer, entry );
30✔
778
    writer.writeEntry( entry );
30✔
779

780
    if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
30✔
781
      simulateColumnChange( currentSchemata[acEntry->tableName], entry );
11✔
782
    else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
19✔
783
      simulateColumnChange( currentSchemata[dcEntry->tableName], entry );
7✔
784
  }
785

786
  for ( const TableSchema &tblModified : schemaModified.tables )
736✔
787
  {
788
    if ( !tblModified.hasPrimaryKey() )
429✔
789
      continue;  // ignore tables without primary key - they can't be compared properly
10✔
790

791
    // Find corresponding table in base DB
792
    const TableSchema *tblBase = nullptr;
422✔
793
    for ( const TableSchema &tbl : schemaBase.tables )
582✔
794
    {
795
      if ( tbl.name == tblModified.name )
579✔
796
      {
797
        tblBase = &tbl;
419✔
798
        break;
419✔
799
      }
800
    }
801

802
    if ( !tblBase )
422✔
803
    {
804
      // Table was newly added, just dump data using INSERTs
805
      dumpTableData( writer, tblModified, true );
3✔
806
      continue;
3✔
807
    }
808

809
    TableDiffContext diffContext = { mDb, *tblBase, tblModified, {}, {}, writer };
419✔
810

811
    for ( const TableColumnInfo &baseColumn : tblBase->columns )
1,926✔
812
    {
813
      for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
3,573✔
814
      {
815
        if ( baseColumn.name == modifiedColumn.name )
3,566✔
816
        {
817
          diffContext.commonColumns.push_back( modifiedColumn );
1,500✔
818
          break;
1,500✔
819
        }
820
      }
821
    }
822

823
    for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
1,930✔
824
    {
825
      bool found = false;
1,511✔
826
      for ( const TableColumnInfo &baseColumn : tblBase->columns )
3,593✔
827
      {
828
        if ( baseColumn.name == modifiedColumn.name )
3,582✔
829
        {
830
          found = true;
1,500✔
831
          break;
1,500✔
832
        }
833
      }
834
      if ( !found )
1,511✔
835
        diffContext.newColumns.push_back( modifiedColumn );
11✔
836
    }
837

838
    handleInserted( context(), diffContext, false );  // INSERT
419✔
839
    handleInserted( context(), diffContext, true );   // DELETE
419✔
840
    handleUpdated( context(), diffContext );          // UPDATE
419✔
841
  }
419✔
842
}
316✔
843

844
static std::string sqlForInsert( const std::string &tableName, const TableSchema &tbl )
155✔
845
{
846
  /*
847
   * For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
848
   *
849
   * INSERT INTO x (a, b, c, d) VALUES (?, ?, ?, ?)
850
   */
851

852
  std::string sql;
155✔
853
  sql += sqlitePrintf( "INSERT INTO \"%w\" (", tableName.c_str() );
155✔
854
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
701✔
855
  {
856
    if ( i > 0 )
546✔
857
      sql += ", ";
391✔
858
    sql += sqlitePrintf( "\"%w\"", tbl.columns[i].name.c_str() );
546✔
859
  }
860
  sql += ") VALUES (";
155✔
861
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
701✔
862
  {
863
    if ( i > 0 )
546✔
864
      sql += ", ";
391✔
865
    sql += "?";
546✔
866
  }
867
  sql += ")";
155✔
868
  return sql;
155✔
869
}
×
870

871
static std::string sqlForUpdate( const std::string &tableName, const TableSchema &tbl )
155✔
872
{
873
  /*
874
  ** For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
875
  **
876
  **     UPDATE x SET
877
  **     a = CASE WHEN ?2  THEN ?3  ELSE a END,
878
  **     b = CASE WHEN ?5  THEN ?6  ELSE b END,
879
  **     c = CASE WHEN ?8  THEN ?9  ELSE c END,
880
  **     d = CASE WHEN ?11 THEN ?12 ELSE d END
881
  **     WHERE a = ?1 AND c = ?7 AND (?13 OR
882
  **       (?5==0 OR b IS ?4) AND (?11==0 OR d IS ?10) AND
883
  **     )
884
  **
885
  ** For each column in the table, there are three variables to bind:
886
  **
887
  **     ?(i*3+1)    The old.* value of the column, if any.
888
  **     ?(i*3+2)    A boolean flag indicating that the value is being modified.
889
  **     ?(i*3+3)    The new.* value of the column, if any.
890
  */
891

892
  std::string sql;
155✔
893
  sql += sqlitePrintf( "UPDATE \"%w\" SET ", tableName.c_str() );
155✔
894

895
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
701✔
896
  {
897
    if ( i > 0 )
546✔
898
      sql += ", ";
391✔
899
    sql += sqlitePrintf( "\"%w\" = CASE WHEN ?%d THEN ?%d ELSE \"%w\" END", tbl.columns[i].name.c_str(), i * 3 + 2, i * 3 + 3, tbl.columns[i].name.c_str() );
546✔
900
  }
901
  sql += " WHERE ";
155✔
902
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
701✔
903
  {
904
    if ( i > 0 )
546✔
905
      sql += " AND ";
391✔
906
    if ( tbl.columns[i].isPrimaryKey )
546✔
907
      sql += sqlitePrintf( " \"%w\" = ?%d ", tbl.columns[i].name.c_str(), i * 3 + 1 );
155✔
908
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
391✔
909
    {
910
      // compare date/time values using datetime() because they may have
911
      // multiple equivalent string representations (see #143)
912
      sql += sqlitePrintf( " ( ?%d = 0 OR STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', \"%w\") IS STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?%d) ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 );
4✔
913
    }
914
    else
915
      sql += sqlitePrintf( " ( ?%d = 0 OR \"%w\" IS ?%d ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 );
387✔
916
  }
917

918
  return sql;
155✔
919
}
×
920

921
static std::string sqlForDelete( const std::string &tableName, const TableSchema &tbl )
155✔
922
{
923
  /*
924
   * For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
925
   *
926
   * DELETE FROM x WHERE a = ? AND b IS ? AND c = ? AND d IS ?
927
   */
928

929
  std::string sql;
155✔
930
  sql += sqlitePrintf( "DELETE FROM \"%w\" WHERE ", tableName.c_str() );
155✔
931
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
701✔
932
  {
933
    if ( i > 0 )
546✔
934
      sql += " AND ";
391✔
935
    if ( tbl.columns[i].isPrimaryKey )
546✔
936
      sql += sqlitePrintf( "\"%w\" = ?", tbl.columns[i].name.c_str() );
155✔
937
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
391✔
938
    {
939
      // compare date/time values using strftime() because otherwise
940
      // fractional seconds will be lost
941
      sql += sqlitePrintf( "STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', \"%w\") IS STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?)", tbl.columns[i].name.c_str() );
4✔
942
    }
943
    else
944
      sql += sqlitePrintf( "\"%w\" IS ?", tbl.columns[i].name.c_str() );
387✔
945
  }
946
  return sql;
155✔
947
}
×
948

949
static void bindValue( sqlite3_stmt *stmt, int index, const Value &v )
1,385✔
950
{
951
  int rc;
952
  if ( v.type() == Value::TypeInt )
1,385✔
953
    rc = sqlite3_bind_int64( stmt, index, v.getInt() );
603✔
954
  else if ( v.type() == Value::TypeDouble )
782✔
955
    rc = sqlite3_bind_double( stmt, index, v.getDouble() );
11✔
956
  else if ( v.type() == Value::TypeNull )
771✔
957
    rc = sqlite3_bind_null( stmt, index );
137✔
958
  else if ( v.type() == Value::TypeText )
634✔
959
    rc = sqlite3_bind_text( stmt, index, v.getString().c_str(), -1, SQLITE_TRANSIENT );
417✔
960
  else if ( v.type() == Value::TypeBlob )
217✔
961
    rc = sqlite3_bind_blob( stmt, index, v.getString().c_str(), ( int ) v.getString().size(), SQLITE_TRANSIENT );
217✔
962
  else
963
    throw GeoDiffException( "unexpected bind type" );
×
964

965
  if ( rc != SQLITE_OK )
1,385✔
966
  {
967
    throw GeoDiffException( "bind failed" );
×
968
  }
969
}
1,385✔
970

971

972
ChangeApplyResult SqliteDriver::applyDataChange( SqliteChangeApplyState &state, const ChangesetDataEntry &entry )
391✔
973
{
974
  std::string tableName = entry.table->name;
391✔
975

976
  if ( startsWith( tableName, "gpkg_" ) ) // skip any changes to GPKG meta tables
782✔
977
    return ChangeApplyResult::Skipped;
4✔
978

979
  if ( context()->isTableSkipped( tableName ) ) // skip table if necessary
387✔
980
    return ChangeApplyResult::Skipped;
10✔
981

982
  if ( state.tableState.count( entry.table ) == 0 )
377✔
983
  {
984
    TableSchema schema = tableSchema( tableName );
155✔
985

986
    if ( schema.columns.size() == 0 )
155✔
987
      throw GeoDiffException( "No such table: " + tableName );
×
988

989
    if ( schema.columns.size() != entry.table->columnCount() )
155✔
990
      throw GeoDiffException( "Wrong number of columns for table: " + tableName );
×
991

992
    for ( size_t i = 0; i < entry.table->columnCount(); ++i )
701✔
993
    {
994
      if ( schema.columns[i].isPrimaryKey != entry.table->primaryKeys[i] )
546✔
995
        throw GeoDiffException( "Mismatch of primary keys in table: " + tableName );
×
996
    }
997

998
    SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table];
155✔
999
    tbl.schema = schema;
155✔
1000

1001
    tbl.stmtInsert.prepare( mDb, sqlForInsert( tableName, schema ) );
155✔
1002
    tbl.stmtUpdate.prepare( mDb, sqlForUpdate( tableName, schema ) );
155✔
1003
    tbl.stmtDelete.prepare( mDb, sqlForDelete( tableName, schema ) );
155✔
1004
  }
155✔
1005
  SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table];
377✔
1006

1007
  if ( entry.op == SQLITE_INSERT )
377✔
1008
  {
1009
    sqlite3_reset( tbl.stmtInsert.get() );
192✔
1010
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
881✔
1011
    {
1012
      const Value &v = entry.newValues[i];
689✔
1013
      bindValue( tbl.stmtInsert.get(), static_cast<int>( i ) + 1, v );
689✔
1014
    }
1015
    int res = sqlite3_step( tbl.stmtInsert.get() );
192✔
1016
    if ( res == SQLITE_CONSTRAINT )
192✔
1017
      return ChangeApplyResult::ConstraintConflict;
11✔
1018
    else if ( res != SQLITE_DONE )
181✔
1019
    {
1020
      logApplyConflict( "insert_failed", entry, true );
×
1021
      throw GeoDiffException( "SQLite error in INSERT" );
×
1022
    }
1023
    else if ( sqlite3_changes( mDb->get() ) != 1 )
181✔
1024
      throw GeoDiffException( "Nothing inserted (this should never happen)" );
×
1025
  }
1026
  else if ( entry.op == SQLITE_UPDATE )
185✔
1027
  {
1028
    sqlite3_reset( tbl.stmtUpdate.get() );
109✔
1029
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
499✔
1030
    {
1031
      const Value &vOld = entry.oldValues[i];
390✔
1032
      const Value &vNew = entry.newValues[i];
390✔
1033
      sqlite3_bind_int( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 2, vNew.type() != Value::TypeUndefined );
390✔
1034
      if ( vOld.type() != Value::TypeUndefined )
390✔
1035
        bindValue( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 1, vOld );
265✔
1036
      if ( vNew.type() != Value::TypeUndefined )
390✔
1037
        bindValue( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 3, vNew );
156✔
1038
    }
1039
    int res = sqlite3_step( tbl.stmtUpdate.get() );
109✔
1040
    if ( res == SQLITE_CONSTRAINT )
109✔
1041
      return ChangeApplyResult::ConstraintConflict;
×
1042
    else if ( res != SQLITE_DONE )
109✔
1043
    {
1044
      logApplyConflict( "update_failed", entry, true );
×
1045
      throw GeoDiffException( "SQLite error in UPDATE" );
×
1046
    }
1047
    else if ( sqlite3_changes( mDb->get() ) == 0 )
109✔
1048
    {
1049
      // either the row with such pkey does not exist or its data have been modified
1050
      logApplyConflict( "update_nothing", entry );
6✔
1051
      return ChangeApplyResult::NoChange;
2✔
1052
    }
1053
  }
1054
  else if ( entry.op == SQLITE_DELETE )
76✔
1055
  {
1056
    sqlite3_reset( tbl.stmtDelete.get() );
76✔
1057
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
351✔
1058
    {
1059
      const Value &v = entry.oldValues[i];
275✔
1060
      bindValue( tbl.stmtDelete.get(), static_cast<int>( i ) + 1, v );
275✔
1061
    }
1062
    int res = sqlite3_step( tbl.stmtDelete.get() );
76✔
1063
    if ( res == SQLITE_CONSTRAINT )
76✔
1064
      return ChangeApplyResult::ConstraintConflict;
×
1065
    else if ( res != SQLITE_DONE )
76✔
1066
    {
1067
      logApplyConflict( "delete_failed", entry, true );
×
1068
      throw GeoDiffException( "SQLite error in DELETE" );
×
1069
    }
1070
    else if ( sqlite3_changes( mDb->get() ) == 0 )
76✔
1071
    {
1072
      // either the row with such pkey does not exist or its data have been modified
1073
      logApplyConflict( "delete_nothing", entry );
3✔
1074
      return ChangeApplyResult::NoChange;
1✔
1075
    }
1076
  }
1077
  else
1078
    throw GeoDiffException( "Unexpected operation" );
×
1079

1080
  return ChangeApplyResult::Applied;
363✔
1081
}
391✔
1082

1083
static void addGpkgCrsDefinition( std::shared_ptr<Sqlite3Db> db, const CrsDefinition &crs )
88✔
1084
{
1085
  // gpkg_spatial_ref_sys
1086
  //   srs_name TEXT NOT NULL, srs_id INTEGER NOT NULL PRIMARY KEY,
1087
  //   organization TEXT NOT NULL, organization_coordsys_id INTEGER NOT NULL,
1088
  //   definition  TEXT NOT NULL, description TEXT
1089

1090
  Sqlite3Stmt stmtCheck;
88✔
1091
  stmtCheck.prepare( db, "select count(*) from gpkg_spatial_ref_sys where srs_id = %d;", crs.srsId );
88✔
1092
  int res = sqlite3_step( stmtCheck.get() );
88✔
1093
  if ( res != SQLITE_ROW )
88✔
1094
  {
1095
    throwSqliteError( db->get(), "Failed to access gpkg_spatial_ref_sys table" );
×
1096
  }
1097

1098
  if ( sqlite3_column_int( stmtCheck.get(), 0 ) )
88✔
1099
    return;  // already there
88✔
1100

NEW
1101
  if ( crs.wkt.size() == 0 )
×
NEW
1102
    throw GeoDiffException( "Tried to add new CRS without WKT definition" );
×
1103

1104
  Sqlite3Stmt stmt;
×
1105
  stmt.prepare( db, "INSERT INTO gpkg_spatial_ref_sys VALUES ('%q:%d', %d, '%q', %d, '%q', '')",
×
1106
                crs.authName.c_str(), crs.authCode, crs.srsId, crs.authName.c_str(), crs.authCode,
×
1107
                crs.wkt.c_str() );
1108
  res = sqlite3_step( stmt.get() );
×
1109
  if ( res != SQLITE_DONE )
×
1110
  {
1111
    throwSqliteError( db->get(), "Failed to insert CRS to gpkg_spatial_ref_sys table" );
×
1112
  }
1113
}
88✔
1114

1115
static void addGpkgSpatialTable( std::shared_ptr<Sqlite3Db> db, const TableSchema &tbl, const Extent &extent )
88✔
1116
{
1117
  size_t i = tbl.geometryColumn();
88✔
1118
  if ( i == SIZE_MAX )
88✔
1119
    throw GeoDiffException( "Adding non-spatial tables is not supported: " + tbl.name );
×
1120

1121
  const TableColumnInfo &col = tbl.columns[i];
88✔
1122
  std::string geomColumn = col.name;
88✔
1123
  std::string geomType = col.geomType;
88✔
1124
  int srsId = col.geomSrsId;
88✔
1125
  bool hasZ = col.geomHasZ;
88✔
1126
  bool hasM = col.geomHasM;
88✔
1127

1128
  // gpkg_contents
1129
  //   table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL,
1130
  //   identifier TEXT, description TEXT DEFAULT '',
1131
  //   last_change DATETIME NOT NULL DEFAULT (...),
1132
  //   min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE,
1133
  //   srs_id INTEGER
1134

1135
  Sqlite3Stmt stmt;
88✔
1136
  stmt.prepare( db, "INSERT INTO gpkg_contents (table_name, data_type, identifier, min_x, min_y, max_x, max_y, srs_id) "
88✔
1137
                "VALUES ('%q', 'features', '%q', %f, %f, %f, %f, %d)",
1138
                tbl.name.c_str(), tbl.name.c_str(), extent.minX, extent.minY, extent.maxX, extent.maxY, srsId );
88✔
1139
  int res = sqlite3_step( stmt.get() );
88✔
1140
  if ( res != SQLITE_DONE )
88✔
1141
  {
1142
    throwSqliteError( db->get(), "Failed to insert row to gpkg_contents table" );
×
1143
  }
1144

1145
  // gpkg_geometry_columns
1146
  //   table_name TEXT NOT NULL, column_name TEXT NOT NULL,
1147
  //   geometry_type_name TEXT NOT NULL, srs_id INTEGER NOT NULL,
1148
  //   z TINYINT NOT NULL,m TINYINT NOT NULL
1149

1150
  Sqlite3Stmt stmtGeomCol;
88✔
1151
  stmtGeomCol.prepare( db, "INSERT INTO gpkg_geometry_columns VALUES ('%q', '%q', '%q', %d, %d, %d)",
88✔
1152
                       tbl.name.c_str(), geomColumn.c_str(), geomType.c_str(), srsId, hasZ, hasM );
1153
  res = sqlite3_step( stmtGeomCol.get() );
88✔
1154
  if ( res != SQLITE_DONE )
88✔
1155
  {
1156
    throwSqliteError( db->get(), "Failed to insert row to gpkg_geometry_columns table" );
×
1157
  }
1158
}
88✔
1159

1160
static void createTable( std::shared_ptr<Sqlite3Db> db, const TableSchema &tbl )
90✔
1161
{
1162
  if ( tbl.geometryColumn() != SIZE_MAX )
90✔
1163
  {
1164
    addGpkgCrsDefinition( db, tbl.crs );
88✔
1165
    addGpkgSpatialTable( db, tbl, Extent() );   // TODO: is it OK to set zeros?
88✔
1166
  }
1167

1168
  std::string sql, pkeyCols, columns;
90✔
1169
  for ( const TableColumnInfo &c : tbl.columns )
371✔
1170
  {
1171
    if ( !columns.empty() )
281✔
1172
      columns += ", ";
191✔
1173

1174
    columns += sqlitePrintf( "\"%w\" %s", c.name.c_str(), c.type.dbType.c_str() );
281✔
1175

1176
    if ( c.isNotNull )
281✔
1177
      columns += " NOT NULL";
24✔
1178

1179
    // we have also c.isAutoIncrement, but the SQLite AUTOINCREMENT keyword only applies
1180
    // to primary keys, and according to the docs, ordinary tables with INTEGER PRIMARY KEY column
1181
    // (which becomes alias to ROWID) does auto-increment, and AUTOINCREMENT just prevents
1182
    // reuse of ROWIDs from previously deleted rows.
1183
    // See https://sqlite.org/autoinc.html
1184

1185
    if ( c.isPrimaryKey )
281✔
1186
    {
1187
      if ( !pkeyCols.empty() )
88✔
NEW
1188
        pkeyCols += ", ";
×
1189
      pkeyCols += sqlitePrintf( "\"%w\"", c.name.c_str() );
88✔
1190
    }
1191
  }
1192

1193
  sql = sqlitePrintf( "CREATE TABLE \"%w\" (", tbl.name.c_str() );
90✔
1194
  if ( !columns.empty() )
90✔
1195
  {
1196
    sql += columns;
90✔
1197
  }
1198
  if ( !pkeyCols.empty() )
90✔
1199
  {
1200
    sql += ", PRIMARY KEY (" + pkeyCols + ")";
88✔
1201
  }
1202
  sql += ");";
90✔
1203

1204
  Sqlite3Stmt stmt;
90✔
1205
  stmt.prepare( db, sql );
90✔
1206
  if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
90✔
1207
  {
NEW
1208
    throwSqliteError( db->get(), "Failure creating table: " + tbl.name );
×
1209
  }
1210
}
90✔
1211

1212
static void removeGpkgSpatialTable( std::shared_ptr<Sqlite3Db> db, const std::string &tableName )
3✔
1213
{
1214
  {
1215
    Sqlite3Stmt stmt;
3✔
1216
    stmt.prepare( db, "DELETE FROM gpkg_contents WHERE table_name = '%q'",
3✔
1217
                  tableName.c_str() );
1218
    int res = sqlite3_step( stmt.get() );
3✔
1219
    if ( res != SQLITE_DONE )
3✔
NEW
1220
      throwSqliteError( db->get(), "Failed to delete table from gpkg_contents table" );
×
1221
  }
3✔
1222

1223
  {
1224
    Sqlite3Stmt stmt;
3✔
1225
    stmt.prepare( db, "DELETE FROM gpkg_geometry_columns WHERE table_name = '%q'",
3✔
1226
                  tableName.c_str() );
1227
    int res = sqlite3_step( stmt.get() );
3✔
1228
    if ( res != SQLITE_DONE )
3✔
NEW
1229
      throwSqliteError( db->get(), "Failed to delete table from gpkg_geometry_columns table" );
×
1230
  }
3✔
1231
}
3✔
1232

1233
void SqliteDriver::applySchemaChange( const ChangesetEntry &entry )
18✔
1234
{
1235
  if ( const ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &entry ) )
18✔
1236
  {
1237
    // TODO: Also save full CRS definition inside diff? It's pretty large and
1238
    // we'd need it for all tables with geometry columns & geometry columns
1239
    // themselves.
1240
    CrsDefinition tableCrs;
3✔
1241
    for ( const TableColumnInfo &col : ctEntry->columns )
12✔
1242
    {
1243
      if ( col.isGeometry )
9✔
1244
        tableCrs.srsId = col.geomSrsId;
2✔
1245
    }
1246

1247
    Sqlite3SavepointTransaction transaction( context(), mDb );
3✔
1248
    try
1249
    {
1250
      createTable( mDb, { ctEntry->tableName, ctEntry->columns, tableCrs } );
3✔
1251
    }
NEW
1252
    catch ( const GeoDiffException & )
×
1253
    {
1254
      // TODO: Make sure this only catches sqlite errors on CREATE TABLE
NEW
1255
      logApplyConflict( "create_table_failed", entry, true );
×
NEW
1256
      throw;
×
NEW
1257
    }
×
1258
    transaction.commitChanges();
3✔
1259
  }
3✔
1260
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
15✔
1261
  {
1262
    // Check there's no data in table (zero rows)
1263
    {
1264
      Sqlite3Stmt stmt;
3✔
1265
      stmt.prepare( mDb, "SELECT COUNT(*) FROM \"%w\"", dtEntry->tableName.c_str() );
3✔
1266
      if ( sqlite3_step( stmt.get() ) != SQLITE_ROW )
3✔
NEW
1267
        throwSqliteError( mDb->get(), "Getting row count in " + dtEntry->tableName );
×
1268
      if ( sqlite3_column_int( stmt.get(), 0 ) != 0 )
3✔
1269
      {
NEW
1270
        logApplyConflict( "drop_table_not_empty", entry );
×
NEW
1271
        throw GeoDiffException( "Tried to drop non-empty table " + dtEntry->tableName );
×
1272
      }
1273
    }
3✔
1274

1275
    Sqlite3Stmt stmt;
3✔
1276
    stmt.prepare( mDb, "DROP TABLE \"%w\"", dtEntry->tableName.c_str() );
3✔
1277
    if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
3✔
1278
    {
NEW
1279
      logApplyConflict( "drop_table_failed", entry, true );
×
NEW
1280
      throwSqliteError( mDb->get(), "Failure deleting table: " + dtEntry->tableName );
×
1281
    }
1282
    removeGpkgSpatialTable( mDb, dtEntry->tableName );
3✔
1283
  }
3✔
1284
  else if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
12✔
1285
  {
1286
    if ( acEntry->column.isGeometry )
7✔
1287
      // Would need changing gpkg metadata
NEW
1288
      throw GeoDiffException( "Adding geometry columns is not supported" );
×
1289
    if ( acEntry->column.isPrimaryKey )
7✔
NEW
1290
      throw GeoDiffException( "Adding column to primary key is not supported" );
×
1291
    if ( acEntry->column.isNotNull )
7✔
NEW
1292
      throw GeoDiffException( "Adding not-null column is not supported" );
×
1293

1294
    std::string sql = sqlitePrintf( "ALTER TABLE \"%w\" ADD COLUMN \"%w\" %s",
1295
                                    acEntry->tableName.c_str(), acEntry->column.name.c_str(), acEntry->column.type.dbType.c_str() );
7✔
1296

1297
    if ( acEntry->column.isNotNull )
7✔
NEW
1298
      sql += " NOT NULL";
×
1299
    Sqlite3Stmt stmt;
7✔
1300
    stmt.prepare( mDb, "%s", sql.c_str() );
7✔
1301
    if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
7✔
1302
    {
NEW
1303
      logApplyConflict( "drop_column_failed", entry, true );
×
NEW
1304
      throwSqliteError( mDb->get(), "Failure adding column: " + acEntry->tableName + "." + acEntry->column.name );
×
1305
    }
1306
  }
7✔
1307
  else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
5✔
1308
  {
1309
    if ( dcEntry->column.isGeometry )
5✔
NEW
1310
      throw GeoDiffException( "Dropping geometry columns is not supported" );
×
1311
    if ( dcEntry->column.isPrimaryKey )
5✔
NEW
1312
      throw GeoDiffException( "Dropping column from primary key is not supported" );
×
1313

1314
    // Check there's no data in the column (all NULLs)
1315
    {
1316
      Sqlite3Stmt stmt;
5✔
1317
      stmt.prepare( mDb, "SELECT COUNT(*) FROM \"%w\" WHERE \"%w\" IS NOT NULL",
5✔
1318
                    dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
1319
      if ( sqlite3_step( stmt.get() ) != SQLITE_ROW )
5✔
NEW
1320
        throwSqliteError( mDb->get(), "Getting row count in " + dcEntry->tableName + "." + dcEntry->column.name );
×
1321
      if ( sqlite3_column_int( stmt.get(), 0 ) != 0 )
5✔
1322
      {
NEW
1323
        logApplyConflict( "drop_column_not_empty", entry );
×
NEW
1324
        throw GeoDiffException( "Tried to drop non-empty column " + dcEntry->tableName + "." + dcEntry->column.name );
×
1325
      }
1326
    }
5✔
1327

1328
    Sqlite3Stmt stmt;
5✔
1329
    stmt.prepare( mDb, "ALTER TABLE \"%w\" DROP COLUMN \"%w\"",
5✔
1330
                  dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
1331
    if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
5✔
1332
    {
NEW
1333
      logApplyConflict( "drop_column_failed", entry, true );
×
NEW
1334
      throwSqliteError( mDb->get(), "Failure deleting column: " + dcEntry->tableName + "." + dcEntry->column.name );
×
1335
    }
1336
  }
5✔
1337
  else
1338
  {
1339
    throw GeoDiffException( "Unhandled entry type (should have been schema change) "
NEW
1340
                            + std::to_string( entry.index() ) );
×
1341
  }
1342
}
21✔
1343

1344
void SqliteDriver::applyChangeset( ChangesetReader &reader )
129✔
1345
{
1346
  TableSchema tbl;
129✔
1347

1348
  // this will acquire DB mutex and release it when the function ends (or when an exception is thrown)
1349
  Sqlite3DbMutexLocker dbMutexLocker( mDb );
129✔
1350

1351
  // start transaction!
1352
  Sqlite3SavepointTransaction savepointTransaction( context(), mDb );
129✔
1353

1354
  // Defer verifying foreign key constraints until end of transaction. This
1355
  // only applies inside our transaction, so we don't need to reset it.
1356
  Sqlite3Stmt statement;
129✔
1357
  statement.prepare( mDb, "pragma defer_foreign_keys = 1" );
129✔
1358
  int rc = sqlite3_step( statement.get() );
129✔
1359
  if ( SQLITE_DONE != rc )
129✔
NEW
1360
    logSqliteError( context(), mDb, "Failed to defer foreign key checks" );
×
1361
  statement.close();
129✔
1362

1363
  // get all triggers sql commands
1364
  // that we do not recognize (gpkg triggers are filtered)
1365
  std::vector<std::string> triggerNames;
129✔
1366
  std::vector<std::string> triggerCmds;
129✔
1367
  sqliteTriggers( context(), mDb, triggerNames, triggerCmds );
129✔
1368

1369
  for ( const std::string &name : triggerNames )
131✔
1370
  {
1371
    statement.prepare( mDb, "drop trigger '%q'", name.c_str() );
2✔
1372
    rc = sqlite3_step( statement.get() );
2✔
1373
    if ( SQLITE_DONE != rc )
2✔
1374
    {
NEW
1375
      logSqliteError( context(), mDb, "Failed to drop trigger " + name );
×
1376
    }
1377
    statement.close();
2✔
1378
  }
1379

1380
  // Applying some entries may fail due to constraints, since they require the
1381
  // entries to be in some specific, unknown order. To work around this, we
1382
  // retry applying the conflicting entries until either we apply them all or we
1383
  // get stuck.
1384
  //
1385
  // We can only reorder data entries, not schema-changing DDL entries, so we
1386
  // gather conflicting data entries in a list until either we run out of
1387
  // entries or read a schema-change entry.
1388

1389
  int unrecoverableConflictCount = 0;
129✔
1390
  std::vector<ChangesetDataEntry> conflictingEntries;
129✔
1391
  ChangesetEntry entry;
129✔
1392
  SqliteChangeApplyState state;
129✔
1393
  while ( true )
1394
  {
1395
    bool haveEntry = reader.nextEntry( entry );
529✔
1396
    if ( !haveEntry || !std::holds_alternative<ChangesetDataEntry>( entry ) )
528✔
1397
    {
1398
      // We can't reorder entries beyond this point (see above), retry applying
1399
      // conflicting ones.
1400
      std::vector<ChangesetDataEntry> newConflictingEntries;
146✔
1401
      while ( conflictingEntries.size() > 0 )
151✔
1402
      {
1403
        for ( const ChangesetDataEntry &centry : conflictingEntries )
16✔
1404
        {
1405
          ChangeApplyResult res = applyDataChange( state, centry );
9✔
1406
          switch ( res )
9✔
1407
          {
1408
            case ChangeApplyResult::Applied:
7✔
1409
            case ChangeApplyResult::Skipped:
1410
              break; // Applied correctly, don't put it in the new list.
7✔
1411
            case ChangeApplyResult::ConstraintConflict:
2✔
1412
              newConflictingEntries.push_back( centry ); // Still conflicting, keep in list.
2✔
1413
              break;
2✔
NEW
1414
            case ChangeApplyResult::NoChange:
×
NEW
1415
              unrecoverableConflictCount++; // Other issue, will throw at the end.
×
NEW
1416
              break;
×
1417
          }
1418
        }
1419

1420
        // If we haven't been able to apply any of the conflicting entries this
1421
        // loop, then these conflicts can't be resolved by reordering entries.
1422
        if ( newConflictingEntries.size() == conflictingEntries.size() )
7✔
1423
        {
1424
          for ( const ChangesetDataEntry &centry : conflictingEntries )
4✔
1425
            logApplyConflict( "unresolvable_conflict", centry );
6✔
1426
          throw GeoDiffConflictsException( "Could not resolve dependencies in constraint conflicts." );
6✔
1427
        }
1428
        conflictingEntries = newConflictingEntries;
5✔
1429
        newConflictingEntries.clear();
5✔
1430
      }
1431
    }
146✔
1432
    if ( !haveEntry )
526✔
1433
      break;
126✔
1434

1435
    if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
400✔
1436
    {
1437
      ChangeApplyResult res = applyDataChange( state, *dataEntry );
382✔
1438
      switch ( res )
382✔
1439
      {
1440
        case ChangeApplyResult::Applied:
370✔
1441
        case ChangeApplyResult::Skipped:
1442
          break; // Applied correctly, continue onward.
370✔
1443
        case ChangeApplyResult::ConstraintConflict:
9✔
1444
          // Ordering conflict found, handle later.
1445
          conflictingEntries.push_back( *dataEntry );
9✔
1446
          break;
9✔
1447
        case ChangeApplyResult::NoChange:
3✔
1448
          unrecoverableConflictCount++; // Other issue, will throw at the end.
3✔
1449
          break;
3✔
1450
      }
1451
    }
1452
    else
1453
    {
1454
      applySchemaChange( entry );
18✔
1455
    }
1456
  }
400✔
1457

1458
  // recreate triggers
1459
  for ( const std::string &cmd : triggerCmds )
128✔
1460
  {
1461
    statement.prepare( mDb, "%s", cmd.c_str() );
2✔
1462
    if ( SQLITE_DONE != sqlite3_step( statement.get() ) )
2✔
1463
    {
NEW
1464
      logSqliteError( context(), mDb, "Failed to recreate trigger using SQL \"" + cmd + "\"" );
×
1465
    }
1466
    statement.close();
2✔
1467
  }
1468

1469
  if ( !unrecoverableConflictCount )
126✔
1470
  {
1471
    savepointTransaction.commitChanges();
123✔
1472
  }
1473
  else
1474
  {
1475
    throw GeoDiffConflictsException( "Conflicts encountered while applying changes! Total " + std::to_string( unrecoverableConflictCount ) );
3✔
1476
  }
1477
}
193✔
1478

1479
void SqliteDriver::createTables( const std::vector<TableSchema> &tables )
78✔
1480
{
1481
  // currently we always create geopackage meta tables. Maybe in the future we can skip
1482
  // that if there is a reason, and have that optional if none of the tables are spatial.
1483

1484
  Sqlite3Stmt stmt1;
78✔
1485
  stmt1.prepare( mDb, "SELECT InitSpatialMetadata('main');" );
78✔
1486
  int res = sqlite3_step( stmt1.get() );
78✔
1487
  if ( res != SQLITE_ROW )
78✔
NEW
1488
    throwSqliteError( mDb->get(), "Failure initializing spatial metadata" );
×
1489

1490
  for ( const TableSchema &tbl : tables )
165✔
1491
  {
1492
    if ( startsWith( tbl.name, "gpkg_" ) )
174✔
NEW
1493
      continue;
×
1494
    createTable( mDb, tbl );
87✔
1495
  }
1496
}
78✔
1497

1498
void SqliteDriver::dumpTableData( ChangesetWriter &writer, TableSchema tbl, bool useModified )
34✔
1499
{
1500
  std::string dbName = databaseName( useModified );
34✔
1501
  if ( !tbl.hasPrimaryKey() )
34✔
1502
    return;  // ignore tables without primary key - they can't be compared properly
1✔
1503

1504
  bool first = true;
33✔
1505
  Sqlite3Stmt statementI;
33✔
1506
  statementI.prepare( mDb, "SELECT * FROM \"%w\".\"%w\"", dbName.c_str(), tbl.name.c_str() );
33✔
1507
  int rc;
1508
  while ( SQLITE_ROW == ( rc = sqlite3_step( statementI.get() ) ) )
102✔
1509
  {
1510
    if ( first )
69✔
1511
    {
1512
      writer.beginTable( schemaToChangesetTable( tbl.name, tbl ) );
32✔
1513
      first = false;
32✔
1514
    }
1515

1516
    ChangesetDataEntry e;
69✔
1517
    e.op = ChangesetDataEntry::OpInsert;
69✔
1518
    size_t numColumns = tbl.columns.size();
69✔
1519
    for ( size_t i = 0; i < numColumns; ++i )
332✔
1520
    {
1521
      Sqlite3Value v( sqlite3_column_value( statementI.get(), static_cast<int>( i ) ) );
263✔
1522
      e.newValues.push_back( changesetValue( v.value() ) );
263✔
1523
    }
263✔
1524
    writer.writeEntry( e );
69✔
1525
  }
69✔
1526
  if ( rc != SQLITE_DONE )
33✔
1527
  {
NEW
1528
    logSqliteError( context(), mDb, "Failure dumping changeset" );
×
1529
  }
1530
}
34✔
1531

1532
void SqliteDriver::dumpData( ChangesetWriter &writer, bool useModified )
14✔
1533
{
1534
  std::vector<std::string> tables = listTables();
14✔
1535
  for ( const std::string &tableName : tables )
45✔
1536
  {
1537
    TableSchema tbl = tableSchema( tableName, useModified );
31✔
1538
    dumpTableData( writer, tbl, useModified );
31✔
1539
  }
31✔
1540
}
14✔
1541

1542
std::vector<std::vector<std::string>> SqliteDriver::executeSql( std::string sql )
140✔
1543
{
1544
  Sqlite3Stmt stmt;
140✔
1545
  stmt.prepare( mDb, "%s", sql.c_str() );
140✔
1546
  std::vector<std::vector<std::string>> rows;
140✔
1547
  int rc;
1548
  while ( ( rc = sqlite3_step( stmt.get() ) ) == SQLITE_ROW )
142✔
1549
  {
1550
    std::vector<std::string> values;
2✔
1551
    values.resize( sqlite3_column_count( stmt.get() ) );
2✔
1552
    for ( size_t i = 0; i < values.size(); ++i )
6✔
1553
    {
1554
      const unsigned char *text = sqlite3_column_text( stmt.get(), static_cast<int>( i ) );
4✔
1555
      if ( text )
4✔
1556
        values[i] = reinterpret_cast<const char *>( text );
4✔
1557
      else
NEW
1558
        values[i] = "<NULL>";
×
1559
    }
1560
    rows.push_back( values );
2✔
1561
  }
2✔
1562
  if ( rc != SQLITE_DONE )
140✔
1563
  {
NEW
1564
    logSqliteError( context(), mDb, "Failure executing SQL: " + sql );
×
1565
  }
1566
  return rows;
280✔
1567
}
140✔
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