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

MerginMaps / geodiff / 26279364240

22 May 2026 09:17AM UTC coverage: 86.421% (-1.7%) from 88.114%
26279364240

Pull #252

github

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

975 of 1160 new or added lines in 13 files covered. (84.05%)

13 existing lines in 3 files now uncovered.

4156 of 4809 relevant lines covered (86.42%)

609.61 hits per line

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

87.89
/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 )
115✔
42
      : mDb( db )
115✔
43
    {
44
      sqlite3_mutex_enter( sqlite3_db_mutex( mDb.get()->get() ) );
115✔
45
    }
115✔
46
    ~Sqlite3DbMutexLocker()
115✔
47
    {
48
      sqlite3_mutex_leave( sqlite3_db_mutex( mDb.get()->get() ) );
115✔
49
    }
115✔
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 )
118✔
66
      : mDb( db ), mContext( context )
118✔
67
    {
68
      if ( sqlite3_exec( mDb.get()->get(), "SAVEPOINT changeset_apply", 0, 0, 0 ) != SQLITE_OK )
118✔
69
      {
70
        throwSqliteError( mDb.get()->get(), "Unable to start savepoint transaction" );
×
71
      }
72
    }
118✔
73

74
    ~Sqlite3SavepointTransaction()
118✔
75
    {
76
      if ( mDb )
118✔
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
    }
118✔
89

90
    void commitChanges()
112✔
91
    {
92
      assert( mDb );
112✔
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 )
112✔
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();
110✔
100
    }
110✔
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 )
560✔
112
  : Driver( context )
560✔
113
{
114
}
560✔
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 )
507✔
119
{
120
  DriverParametersMap::const_iterator connBaseIt = conn.find( "base" );
507✔
121
  if ( connBaseIt == conn.end() )
507✔
122
    throw GeoDiffException( "Missing 'base' file" );
3✔
123

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

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

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

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

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

145
    {
146
      Buffer sqlBuf;
291✔
147
      sqlBuf.printf( "ATTACH '%q' AS modified", modified.c_str() );
291✔
148
      mDb->exec( sqlBuf );
291✔
149
    }
291✔
150
  }
293✔
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 ) )
497✔
155
  {
156
    register_gpkg_extensions( mDb );
418✔
157
  }
158

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

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

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

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

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

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

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

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

215
    std::string tableName( name );
15,662✔
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_" ) )
15,662✔
233
      continue;
4,284✔
234
    // table handled by triggers rtree_*_geometry_*
235
    if ( startsWith( tableName, "rtree_" ) )
7,094✔
236
      continue;
1,968✔
237
    // internal table for AUTOINCREMENT
238
    if ( tableName == "sqlite_sequence" )
1,579✔
239
      continue;
575✔
240

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

244
    tableNames.push_back( tableName );
985✔
245
  }
7,831✔
246
  if ( rc != SQLITE_DONE )
679✔
247
  {
248
    logSqliteError( context(), mDb, "Failed to list SQLite tables" );
×
249
  }
250

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

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

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

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

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

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

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

290
    tbl.columns.push_back( columnInfo );
4,094✔
291
  }
4,094✔
292
  if ( rc != SQLITE_DONE )
1,131✔
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,262✔
299
  {
300
    //
301
    // get geometry column details (geometry type, whether it has Z/M values, CRS id)
302
    //
303

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

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

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

326
      TableColumnInfo &col = tbl.columns[i];
824✔
327
      col.setGeometry( geomTypeName, srsId, hasM, hasZ );
824✔
328
    }
824✔
329
    if ( rc != SQLITE_DONE )
913✔
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 )
913✔
339
    {
340
      Sqlite3Stmt stmtCrs;
750✔
341
      stmtCrs.prepare( mDb, "SELECT * FROM \"%w\".gpkg_spatial_ref_sys WHERE srs_id = %d", dbName.c_str(), srsId );
750✔
342
      if ( SQLITE_ROW != sqlite3_step( stmtCrs.get() ) )
750✔
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 );
750✔
348
      const unsigned char *chrWkt = sqlite3_column_text( stmtCrs.get(), 4 );
750✔
349
      if ( chrAuthName == nullptr )
750✔
350
        throw GeoDiffException( "NULL auth name in gpkg_spatial_ref_sys: " + tableName );
×
351
      if ( chrWkt == nullptr )
750✔
352
        throw GeoDiffException( "NULL definition in gpkg_spatial_ref_sys: " + tableName );
×
353

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

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

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

375
  return tbl;
2,262✔
376
}
1,133✔
377

378
DatabaseSchema SqliteDriver::getSchema( bool useModified )
578✔
379
{
380
  std::vector<TableSchema> tables;
578✔
381
  for ( const std::string &name : listTables( useModified ) )
1,396✔
382
  {
383
    tables.push_back( tableSchema( name, useModified ) );
818✔
384
  }
578✔
385
  return {tables};
1,156✔
386
}
578✔
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, ... )
11,925✔
393
{
394
  va_list ap;
395
  va_start( ap, zFormat );
11,925✔
396
  char *zSql = sqlite3_vmprintf( zFormat, ap );
11,925✔
397
  va_end( ap );
11,925✔
398

399
  if ( zSql == nullptr )
11,925✔
400
  {
401
    throw GeoDiffException( "out of memory" );
×
402
  }
403
  std::string res = reinterpret_cast<const char *>( zSql );
11,925✔
404
  sqlite3_free( zSql );
11,925✔
405
  return res;
23,850✔
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,588✔
420
{
421
  const char *tableName = ( reverse ? diffContext.schemaBase.name : diffContext.schemaModified.name ).c_str();
1,588✔
422

423
  std::string colsStr; // Column list equivalent to modified schema
1,588✔
424
  for ( const TableColumnInfo &c : diffContext.schemaModified.columns )
7,328✔
425
  {
426
    if ( !colsStr.empty() )
5,740✔
427
      colsStr += ", ";
4,152✔
428
    if ( reverse )
5,740✔
429
    {
430
      // Check if this column also exists in base and NULL it out if not
431
      bool found = false;
2,870✔
432
      for ( const auto &commonCol : diffContext.commonColumns )
6,834✔
433
      {
434
        if ( commonCol.name == c.name )
6,820✔
435
        {
436
          found = true;
2,856✔
437
          break;
2,856✔
438
        }
439
      }
440
      if ( !found )
2,870✔
441
      {
442
        colsStr += sqlitePrintf( "NULL AS \"%w\"", c.name.c_str() );
14✔
443
        continue;
14✔
444
      }
445
    }
446
    colsStr += sqlitePrintf( "\"%w\".\"%w\".\"%w\"",
11,452✔
447
                             reverse ? "main" : "modified", tableName, c.name.c_str() );
5,726✔
448
  }
449
  return colsStr;
1,588✔
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 )
794✔
454
{
455
  const char *baseTableName = diffContext.schemaBase.name.c_str();
794✔
456
  const char *modifiedTableName = diffContext.schemaModified.name.c_str();
794✔
457

458
  std::string exprPk; // Filter expression checking primary key is equal
794✔
459
  for ( const TableColumnInfo &c : diffContext.commonColumns )
3,650✔
460
  {
461
    if ( c.isPrimaryKey )
2,856✔
462
    {
463
      if ( !exprPk.empty() )
802✔
464
        exprPk += " AND ";
8✔
465
      exprPk += sqlitePrintf( "\"modified\".\"%w\".\"%w\"=\"main\".\"%w\".\"%w\"",
1,604✔
466
                              modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
802✔
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,588✔
472
                                  reverse ? "main" : "modified", reverse ? baseTableName : modifiedTableName,
473
                                  reverse ? "modified" : "main", reverse ? modifiedTableName : baseTableName, exprPk.c_str() );
1,588✔
474
  return sql;
1,588✔
475
}
794✔
476

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

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

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

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

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

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

516
  if ( exprOther.empty() )
397✔
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() );
396✔
525
  }
526
}
397✔
527

528

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

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

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

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

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

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

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

589
  Sqlite3Stmt statement;
397✔
590
  statement.prepare( diffContext.db, "%s", sqlModified.c_str() );
397✔
591
  int rc;
592
  while ( SQLITE_ROW == ( rc = sqlite3_step( statement.get() ) ) )
479✔
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;
82✔
606
    e.op = ChangesetDataEntry::OpUpdate;
82✔
607

608
    bool hasUpdates = false;
82✔
609
    size_t numColumns = diffContext.schemaModified.columns.size();
82✔
610
    for ( size_t i = 0; i < numColumns; ++i )
401✔
611
    {
612
      Sqlite3Value v1( sqlite3_column_value( statement.get(), static_cast<int>( i + numColumns ) ) );
319✔
613
      Sqlite3Value v2( sqlite3_column_value( statement.get(), static_cast<int>( i ) ) );
319✔
614
      bool pkey = diffContext.schemaModified.columns[i].isPrimaryKey;
319✔
615
      bool updated = ( v1 != v2 );
319✔
616
      if ( updated )
319✔
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 )
95✔
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 )
95✔
640
        {
641
          hasUpdates = true;
93✔
642
        }
643
      }
644
      e.oldValues.push_back( ( pkey || updated ) ? changesetValue( v1.value() ) : Value() );
494✔
645
      e.newValues.push_back( updated ? changesetValue( v2.value() ) : Value() );
319✔
646
    }
319✔
647

648
    if ( hasUpdates )
82✔
649
    {
650
      if ( !diffContext.tableEntryWritten )
80✔
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 );
80✔
658
    }
659
  }
82✔
660
  if ( rc != SQLITE_DONE )
397✔
661
  {
NEW
662
    logSqliteError( context, diffContext.db, "Failed to write information about inserted rows in table " + diffContext.schemaModified.name );
×
663
  }
664
}
397✔
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 )
24✔
669
{
670
  if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
24✔
671
  {
672
    auto it = currentSchemata.find( dcEntry->tableName );
5✔
673
    if ( it == currentSchemata.end() )
5✔
NEW
674
      throw GeoDiffException( "Missing schema for table " + dcEntry->tableName );
×
675
    const TableSchema &table = it->second;
5✔
676

677
    std::string pkeyColStr;
5✔
678
    for ( const TableColumnInfo &c : table.columns )
24✔
679
    {
680
      if ( c.isPrimaryKey )
19✔
681
      {
682
        if ( !pkeyColStr.empty() )
5✔
NEW
683
          pkeyColStr += ", ";
×
684
        pkeyColStr += sqlitePrintf( "\"%w\"", c.name.c_str() );
5✔
685
      }
686
    }
687
    if ( pkeyColStr.empty() )
5✔
NEW
688
      throw GeoDiffException( "Table " + table.name + " has no primary key" );
×
689

690
    Sqlite3Stmt stmt;
5✔
691
    stmt.prepare( db, "SELECT %s, \"%w\" FROM \"main\".\"%w\" WHERE \"%w\" IS NOT NULL",
5✔
692
                  pkeyColStr.c_str(), dcEntry->column.name.c_str(), dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
693

694
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
5✔
695
    int rc;
696
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
20✔
697
    {
698
      ChangesetDataEntry e;
15✔
699
      e.op = ChangesetDataEntry::OpUpdate;
15✔
700

701
      size_t idxInResult = 0;
15✔
702
      for ( size_t i = 0; i < table.columns.size(); ++i )
72✔
703
      {
704
        bool isPkey = table.columns[i].isPrimaryKey;
57✔
705
        bool isDroppedCol = ( i == table.columns.size() - 1 );
57✔
706

707
        if ( isPkey || isDroppedCol )
57✔
708
        {
709
          Sqlite3Value v( sqlite3_column_value( stmt.get(), static_cast<int>( idxInResult ) ) );
30✔
710
          e.oldValues.push_back( changesetValue( v.value() ) );
30✔
711
          idxInResult++;
30✔
712
        }
30✔
713
        else
714
          e.oldValues.push_back( Value() );
27✔
715

716
        if ( isDroppedCol )
57✔
717
        {
718
          Value nullVal;
15✔
719
          nullVal.setNull();
15✔
720
          e.newValues.push_back( nullVal );
15✔
721
        }
15✔
722
        else
723
          e.newValues.push_back( Value() );
42✔
724
      }
725

726
      writer.writeEntry( e );
15✔
727
    }
15✔
728
  }
5✔
729
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
19✔
730
  {
731
    auto it = currentSchemata.find( dtEntry->tableName );
5✔
732
    if ( it == currentSchemata.end() )
5✔
NEW
733
      throw GeoDiffException( "Missing schema for table " + dtEntry->tableName );
×
734
    const TableSchema &table = it->second;
5✔
735

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

739
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
5✔
740
    int rc;
741
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
16✔
742
    {
743
      ChangesetDataEntry e;
11✔
744
      e.op = ChangesetDataEntry::OpDelete;
11✔
745

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

753
      writer.writeEntry( e );
11✔
754
    }
11✔
755
  }
5✔
756
}
24✔
757

758
void SqliteDriver::createChangeset( ChangesetWriter &writer )
289✔
759
{
760
  DatabaseSchema schemaBase = getSchema( false );
289✔
761
  DatabaseSchema schemaModified = getSchema( true );
289✔
762

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

770
  auto schemaDiffEntries = diffDatabaseSchema( schemaBase, schemaModified );
289✔
771
  for ( const ChangesetEntry &entry : schemaDiffEntries )
310✔
772
  {
773
    writeDataChangesForSchemaChange( mDb, currentSchemata, writer, entry );
24✔
774
    writer.writeEntry( entry );
24✔
775

776
    if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
24✔
777
      simulateColumnChange( currentSchemata[acEntry->tableName], entry );
7✔
778
    else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
17✔
779
      simulateColumnChange( currentSchemata[dcEntry->tableName], entry );
5✔
780
  }
781

782
  for ( const TableSchema &tblModified : schemaModified.tables )
693✔
783
  {
784
    if ( !tblModified.hasPrimaryKey() )
407✔
785
      continue;  // ignore tables without primary key - they can't be compared properly
10✔
786

787
    // Find corresponding table in base DB
788
    const TableSchema *tblBase = nullptr;
400✔
789
    for ( const TableSchema &tbl : schemaBase.tables )
559✔
790
    {
791
      if ( tbl.name == tblModified.name )
556✔
792
      {
793
        tblBase = &tbl;
397✔
794
        break;
397✔
795
      }
796
    }
797

798
    if ( !tblBase )
400✔
799
    {
800
      // Table was newly added, just dump data using INSERTs
801
      dumpTableData( writer, tblModified, true );
3✔
802
      continue;
3✔
803
    }
804

805
    TableDiffContext diffContext = { mDb, *tblBase, tblModified, {}, {}, writer };
397✔
806

807
    for ( const TableColumnInfo &baseColumn : tblBase->columns )
1,830✔
808
    {
809
      for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
3,407✔
810
      {
811
        if ( baseColumn.name == modifiedColumn.name )
3,402✔
812
        {
813
          diffContext.commonColumns.push_back( modifiedColumn );
1,428✔
814
          break;
1,428✔
815
        }
816
      }
817
    }
818

819
    for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
1,832✔
820
    {
821
      bool found = false;
1,435✔
822
      for ( const TableColumnInfo &baseColumn : tblBase->columns )
3,418✔
823
      {
824
        if ( baseColumn.name == modifiedColumn.name )
3,411✔
825
        {
826
          found = true;
1,428✔
827
          break;
1,428✔
828
        }
829
      }
830
      if ( !found )
1,435✔
831
        diffContext.newColumns.push_back( modifiedColumn );
7✔
832
    }
833

834
    handleInserted( context(), diffContext, false );  // INSERT
397✔
835
    handleInserted( context(), diffContext, true );   // DELETE
397✔
836
    handleUpdated( context(), diffContext );          // UPDATE
397✔
837
  }
397✔
838
}
295✔
839

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

848
  std::string sql;
141✔
849
  sql += sqlitePrintf( "INSERT INTO \"%w\" (", tableName.c_str() );
141✔
850
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
639✔
851
  {
852
    if ( i > 0 )
498✔
853
      sql += ", ";
357✔
854
    sql += sqlitePrintf( "\"%w\"", tbl.columns[i].name.c_str() );
498✔
855
  }
856
  sql += ") VALUES (";
141✔
857
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
639✔
858
  {
859
    if ( i > 0 )
498✔
860
      sql += ", ";
357✔
861
    sql += "?";
498✔
862
  }
863
  sql += ")";
141✔
864
  return sql;
141✔
865
}
×
866

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

888
  std::string sql;
141✔
889
  sql += sqlitePrintf( "UPDATE \"%w\" SET ", tableName.c_str() );
141✔
890

891
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
639✔
892
  {
893
    if ( i > 0 )
498✔
894
      sql += ", ";
357✔
895
    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() );
498✔
896
  }
897
  sql += " WHERE ";
141✔
898
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
639✔
899
  {
900
    if ( i > 0 )
498✔
901
      sql += " AND ";
357✔
902
    if ( tbl.columns[i].isPrimaryKey )
498✔
903
      sql += sqlitePrintf( " \"%w\" = ?%d ", tbl.columns[i].name.c_str(), i * 3 + 1 );
141✔
904
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
357✔
905
    {
906
      // compare date/time values using datetime() because they may have
907
      // multiple equivalent string representations (see #143)
908
      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✔
909
    }
910
    else
911
      sql += sqlitePrintf( " ( ?%d = 0 OR \"%w\" IS ?%d ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 );
353✔
912
  }
913

914
  return sql;
141✔
915
}
×
916

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

925
  std::string sql;
141✔
926
  sql += sqlitePrintf( "DELETE FROM \"%w\" WHERE ", tableName.c_str() );
141✔
927
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
639✔
928
  {
929
    if ( i > 0 )
498✔
930
      sql += " AND ";
357✔
931
    if ( tbl.columns[i].isPrimaryKey )
498✔
932
      sql += sqlitePrintf( "\"%w\" = ?", tbl.columns[i].name.c_str() );
141✔
933
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
357✔
934
    {
935
      // compare date/time values using strftime() because otherwise
936
      // fractional seconds will be lost
937
      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✔
938
    }
939
    else
940
      sql += sqlitePrintf( "\"%w\" IS ?", tbl.columns[i].name.c_str() );
353✔
941
  }
942
  return sql;
141✔
943
}
×
944

945
static void bindValue( sqlite3_stmt *stmt, int index, const Value &v )
1,265✔
946
{
947
  int rc;
948
  if ( v.type() == Value::TypeInt )
1,265✔
949
    rc = sqlite3_bind_int64( stmt, index, v.getInt() );
551✔
950
  else if ( v.type() == Value::TypeDouble )
714✔
951
    rc = sqlite3_bind_double( stmt, index, v.getDouble() );
11✔
952
  else if ( v.type() == Value::TypeNull )
703✔
953
    rc = sqlite3_bind_null( stmt, index );
115✔
954
  else if ( v.type() == Value::TypeText )
588✔
955
    rc = sqlite3_bind_text( stmt, index, v.getString().c_str(), -1, SQLITE_TRANSIENT );
387✔
956
  else if ( v.type() == Value::TypeBlob )
201✔
957
    rc = sqlite3_bind_blob( stmt, index, v.getString().c_str(), ( int ) v.getString().size(), SQLITE_TRANSIENT );
201✔
958
  else
959
    throw GeoDiffException( "unexpected bind type" );
×
960

961
  if ( rc != SQLITE_OK )
1,265✔
962
  {
963
    throw GeoDiffException( "bind failed" );
×
964
  }
965
}
1,265✔
966

967

968
ChangeApplyResult SqliteDriver::applyDataChange( SqliteChangeApplyState &state, const ChangesetDataEntry &entry )
353✔
969
{
970
  std::string tableName = entry.table->name;
353✔
971

972
  if ( startsWith( tableName, "gpkg_" ) ) // skip any changes to GPKG meta tables
706✔
973
    return ChangeApplyResult::Skipped;
4✔
974

975
  if ( context()->isTableSkipped( tableName ) ) // skip table if necessary
349✔
976
    return ChangeApplyResult::Skipped;
6✔
977

978
  if ( state.tableState.count( entry.table.get() ) == 0 )
343✔
979
  {
980
    TableSchema schema = tableSchema( tableName );
141✔
981

982
    if ( schema.columns.size() == 0 )
141✔
983
      throw GeoDiffException( "No such table: " + tableName );
×
984

985
    if ( schema.columns.size() != entry.table->columnCount() )
141✔
986
      throw GeoDiffException( "Wrong number of columns for table: " + tableName );
×
987

988
    for ( size_t i = 0; i < entry.table->columnCount(); ++i )
639✔
989
    {
990
      if ( schema.columns[i].isPrimaryKey != entry.table->primaryKeys[i] )
498✔
991
        throw GeoDiffException( "Mismatch of primary keys in table: " + tableName );
×
992
    }
993

994
    SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table.get()];
141✔
995
    tbl.schema = schema;
141✔
996

997
    tbl.stmtInsert.prepare( mDb, sqlForInsert( tableName, schema ) );
141✔
998
    tbl.stmtUpdate.prepare( mDb, sqlForUpdate( tableName, schema ) );
141✔
999
    tbl.stmtDelete.prepare( mDb, sqlForDelete( tableName, schema ) );
141✔
1000
  }
141✔
1001
  SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table.get()];
343✔
1002

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

1076
  return ChangeApplyResult::Applied;
329✔
1077
}
353✔
1078

1079
static void addGpkgCrsDefinition( std::shared_ptr<Sqlite3Db> db, const CrsDefinition &crs )
63✔
1080
{
1081
  // gpkg_spatial_ref_sys
1082
  //   srs_name TEXT NOT NULL, srs_id INTEGER NOT NULL PRIMARY KEY,
1083
  //   organization TEXT NOT NULL, organization_coordsys_id INTEGER NOT NULL,
1084
  //   definition  TEXT NOT NULL, description TEXT
1085

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

1094
  if ( sqlite3_column_int( stmtCheck.get(), 0 ) )
63✔
1095
    return;  // already there
63✔
1096

NEW
1097
  if ( crs.wkt.size() == 0 )
×
NEW
1098
    throw GeoDiffException( "Tried to add new CRS without WKT definition" );
×
1099

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

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

1117
  const TableColumnInfo &col = tbl.columns[i];
63✔
1118
  std::string geomColumn = col.name;
63✔
1119
  std::string geomType = col.geomType;
63✔
1120
  int srsId = col.geomSrsId;
63✔
1121
  bool hasZ = col.geomHasZ;
63✔
1122
  bool hasM = col.geomHasM;
63✔
1123

1124
  // gpkg_contents
1125
  //   table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL,
1126
  //   identifier TEXT, description TEXT DEFAULT '',
1127
  //   last_change DATETIME NOT NULL DEFAULT (...),
1128
  //   min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE,
1129
  //   srs_id INTEGER
1130

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

1141
  // gpkg_geometry_columns
1142
  //   table_name TEXT NOT NULL, column_name TEXT NOT NULL,
1143
  //   geometry_type_name TEXT NOT NULL, srs_id INTEGER NOT NULL,
1144
  //   z TINYINT NOT NULL,m TINYINT NOT NULL
1145

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

1156
static void createTable( std::shared_ptr<Sqlite3Db> db, const TableSchema &tbl )
65✔
1157
{
1158
  if ( tbl.geometryColumn() != SIZE_MAX )
65✔
1159
  {
1160
    addGpkgCrsDefinition( db, tbl.crs );
63✔
1161
    addGpkgSpatialTable( db, tbl, Extent() );   // TODO: is it OK to set zeros?
63✔
1162
  }
1163

1164
  std::string sql, pkeyCols, columns;
65✔
1165
  for ( const TableColumnInfo &c : tbl.columns )
271✔
1166
  {
1167
    if ( !columns.empty() )
206✔
1168
      columns += ", ";
141✔
1169

1170
    columns += sqlitePrintf( "\"%w\" %s", c.name.c_str(), c.type.dbType.c_str() );
206✔
1171

1172
    if ( c.isNotNull )
206✔
1173
      columns += " NOT NULL";
24✔
1174

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

1181
    if ( c.isPrimaryKey )
206✔
1182
    {
1183
      if ( !pkeyCols.empty() )
63✔
NEW
1184
        pkeyCols += ", ";
×
1185
      pkeyCols += sqlitePrintf( "\"%w\"", c.name.c_str() );
63✔
1186
    }
1187
  }
1188

1189
  sql = sqlitePrintf( "CREATE TABLE \"%w\" (", tbl.name.c_str() );
65✔
1190
  if ( !columns.empty() )
65✔
1191
  {
1192
    sql += columns;
65✔
1193
  }
1194
  if ( !pkeyCols.empty() )
65✔
1195
  {
1196
    sql += ", PRIMARY KEY (" + pkeyCols + ")";
63✔
1197
  }
1198
  sql += ");";
65✔
1199

1200
  Sqlite3Stmt stmt;
65✔
1201
  stmt.prepare( db, sql );
65✔
1202
  if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
65✔
1203
  {
NEW
1204
    throwSqliteError( db->get(), "Failure creating table: " + tbl.name );
×
1205
  }
1206
}
65✔
1207

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

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

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

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

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

1288
    std::string sql = sqlitePrintf( "ALTER TABLE \"%w\" ADD COLUMN \"%w\" %s",
1289
                                    acEntry->tableName.c_str(), acEntry->column.name.c_str(), acEntry->column.type.dbType.c_str() );
3✔
1290

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

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

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

1338
void SqliteDriver::applyChangeset( ChangesetReader &reader )
115✔
1339
{
1340
  TableSchema tbl;
115✔
1341

1342
  // this will acquire DB mutex and release it when the function ends (or when an exception is thrown)
1343
  Sqlite3DbMutexLocker dbMutexLocker( mDb );
115✔
1344

1345
  // start transaction!
1346
  Sqlite3SavepointTransaction savepointTransaction( context(), mDb );
115✔
1347

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

1357
  // get all triggers sql commands
1358
  // that we do not recognize (gpkg triggers are filtered)
1359
  std::vector<std::string> triggerNames;
115✔
1360
  std::vector<std::string> triggerCmds;
115✔
1361
  sqliteTriggers( context(), mDb, triggerNames, triggerCmds );
115✔
1362

1363
  for ( const std::string &name : triggerNames )
117✔
1364
  {
1365
    statement.prepare( mDb, "drop trigger '%q'", name.c_str() );
2✔
1366
    rc = sqlite3_step( statement.get() );
2✔
1367
    if ( SQLITE_DONE != rc )
2✔
1368
    {
NEW
1369
      logSqliteError( context(), mDb, "Failed to drop trigger " + name );
×
1370
    }
1371
    statement.close();
2✔
1372
  }
1373

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

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

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

1429
    if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
356✔
1430
    {
1431
      ChangeApplyResult res = applyDataChange( state, *dataEntry );
344✔
1432
      switch ( res )
344✔
1433
      {
1434
        case ChangeApplyResult::Applied:
332✔
1435
        case ChangeApplyResult::Skipped:
1436
          break; // Applied correctly, continue onward.
332✔
1437
        case ChangeApplyResult::ConstraintConflict:
9✔
1438
          // Ordering conflict found, handle later.
1439
          conflictingEntries.push_back( *dataEntry );
9✔
1440
          break;
9✔
1441
        case ChangeApplyResult::NoChange:
3✔
1442
          unrecoverableConflictCount++; // Other issue, will throw at the end.
3✔
1443
          break;
3✔
1444
      }
1445
    }
1446
    else
1447
    {
1448
      applySchemaChange( entry );
12✔
1449
    }
1450
  }
356✔
1451

1452
  // recreate triggers
1453
  for ( const std::string &cmd : triggerCmds )
114✔
1454
  {
1455
    statement.prepare( mDb, "%s", cmd.c_str() );
2✔
1456
    if ( SQLITE_DONE != sqlite3_step( statement.get() ) )
2✔
1457
    {
NEW
1458
      logSqliteError( context(), mDb, "Failed to recreate trigger using SQL \"" + cmd + "\"" );
×
1459
    }
1460
    statement.close();
2✔
1461
  }
1462

1463
  if ( !unrecoverableConflictCount )
112✔
1464
  {
1465
    savepointTransaction.commitChanges();
109✔
1466
  }
1467
  else
1468
  {
1469
    throw GeoDiffConflictsException( "Conflicts encountered while applying changes! Total " + std::to_string( unrecoverableConflictCount ) );
3✔
1470
  }
1471
}
179✔
1472

1473
void SqliteDriver::createTables( const std::vector<TableSchema> &tables )
53✔
1474
{
1475
  // currently we always create geopackage meta tables. Maybe in the future we can skip
1476
  // that if there is a reason, and have that optional if none of the tables are spatial.
1477

1478
  Sqlite3Stmt stmt1;
53✔
1479
  stmt1.prepare( mDb, "SELECT InitSpatialMetadata('main');" );
53✔
1480
  int res = sqlite3_step( stmt1.get() );
53✔
1481
  if ( res != SQLITE_ROW )
53✔
NEW
1482
    throwSqliteError( mDb->get(), "Failure initializing spatial metadata" );
×
1483

1484
  for ( const TableSchema &tbl : tables )
115✔
1485
  {
1486
    if ( startsWith( tbl.name, "gpkg_" ) )
124✔
NEW
1487
      continue;
×
1488
    createTable( mDb, tbl );
62✔
1489
  }
1490
}
53✔
1491

1492
void SqliteDriver::dumpTableData( ChangesetWriter &writer, TableSchema tbl, bool useModified )
34✔
1493
{
1494
  std::string dbName = databaseName( useModified );
34✔
1495
  if ( !tbl.hasPrimaryKey() )
34✔
1496
    return;  // ignore tables without primary key - they can't be compared properly
1✔
1497

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

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

1526
void SqliteDriver::dumpData( ChangesetWriter &writer, bool useModified )
14✔
1527
{
1528
  std::vector<std::string> tables = listTables();
14✔
1529
  for ( const std::string &tableName : tables )
45✔
1530
  {
1531
    TableSchema tbl = tableSchema( tableName, useModified );
31✔
1532
    dumpTableData( writer, tbl, useModified );
31✔
1533
  }
31✔
1534
}
14✔
1535

1536
std::vector<std::vector<std::string>> SqliteDriver::executeSql( std::string sql )
86✔
1537
{
1538
  Sqlite3Stmt stmt;
86✔
1539
  stmt.prepare( mDb, "%s", sql.c_str() );
86✔
1540
  std::vector<std::vector<std::string>> rows;
86✔
1541
  int rc;
1542
  while ( ( rc = sqlite3_step( stmt.get() ) ) == SQLITE_ROW )
86✔
1543
  {
NEW
1544
    std::vector<std::string> values;
×
NEW
1545
    values.resize( sqlite3_column_count( stmt.get() ) );
×
NEW
1546
    for ( size_t i = 0; i < values.size(); ++i )
×
1547
    {
NEW
1548
      const unsigned char *text = sqlite3_column_text( stmt.get(), static_cast<int>( i ) );
×
NEW
1549
      values.push_back( reinterpret_cast<const char *>( text ) );
×
1550
    }
NEW
1551
    rows.push_back( values );
×
NEW
1552
  }
×
1553
  if ( rc != SQLITE_DONE )
86✔
1554
  {
NEW
1555
    logSqliteError( context(), mDb, "Failure executing SQL: " + sql );
×
1556
  }
1557
  return rows;
172✔
1558
}
86✔
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