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

MerginMaps / geodiff / 30586645802

30 Jul 2026 10:18PM UTC coverage: 87.091% (-0.5%) from 87.592%
30586645802

Pull #252

github

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

1317 of 1483 new or added lines in 13 files covered. (88.81%)

15 existing lines in 4 files now uncovered.

4473 of 5136 relevant lines covered (87.09%)

677.01 hits per line

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

87.0
/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 <algorithm>
21
#include <memory.h>
22
#include <sqlite3.h>
23
#include <unordered_map>
24
#include <variant>
25

26

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

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

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

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

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

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

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

108

109
///////
110

111

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

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

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

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

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

137
  if ( mHasModified )
594✔
138
  {
139
    std::string modified = connModifiedIt->second;
356✔
140

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

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

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

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

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

172
  std::string base = connBaseIt->second;
123✔
173

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

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

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

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

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

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

232
    // table handled by triggers trigger_*_feature_count_*
233
    if ( startsWith( tableName, "gpkg_" ) )
19,026✔
234
      continue;
5,608✔
235
    // table handled by triggers rtree_*_geometry_*
236
    if ( startsWith( tableName, "rtree_" ) )
7,810✔
237
      continue;
2,040✔
238
    // internal table for AUTOINCREMENT
239
    if ( tableName == "sqlite_sequence" )
1,865✔
240
      continue;
711✔
241

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

245
    tableNames.push_back( tableName );
1,125✔
246
  }
9,513✔
247
  if ( rc != SQLITE_DONE )
815✔
248
  {
249
    logSqliteError( context(), mDb, "Failed to list SQLite tables" );
×
250
  }
251

252
  // result is ordered by name
253
  return tableNames;
1,630✔
254
}
815✔
255

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

264
std::tuple<std::vector<SqliteColumnInfo>, CrsDefinition> SqliteDriver::sqliteColumns( const std::string &dbName, const std::string &tableName )
1,417✔
265
{
266
  std::vector<SqliteColumnInfo> defs;
1,417✔
267
  CrsDefinition crs;
1,417✔
268

269
  Sqlite3Stmt stmt;
1,417✔
270
  stmt.prepare( mDb, "PRAGMA '%q'.table_xinfo('%q')", dbName.c_str(), tableName.c_str() );
1,417✔
271
  int rc;
272
  while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
6,486✔
273
  {
274
    const unsigned char *name = sqlite3_column_text( stmt.get(), 1 );
5,069✔
275
    if ( name == nullptr )
5,069✔
UNCOV
276
      throw GeoDiffException( "NULL column name in table schema: " + tableName );
×
277
    const unsigned char *type = sqlite3_column_text( stmt.get(), 2 );
5,069✔
278
    const unsigned char *defaultValue = sqlite3_column_text( stmt.get(), 4 );
5,069✔
279

280
    SqliteColumnInfo def;
5,069✔
281
    def.column.name = reinterpret_cast<const char *>( name );
5,069✔
282
    def.column.type.dbType = type ? reinterpret_cast<const char *>( type ) : "";
5,069✔
283
    def.column.isNotNull = sqlite3_column_int( stmt.get(), 3 );
5,069✔
284
    def.column.isPrimaryKey = sqlite3_column_int( stmt.get(), 5 );
5,069✔
285
    def.defaultValue = defaultValue ? reinterpret_cast<const char *>( defaultValue ) : "";
5,069✔
286
    def.hidden = sqlite3_column_int( stmt.get(), 6 );
5,069✔
287
    defs.push_back( def );
5,069✔
288
  }
5,069✔
289
  if ( rc != SQLITE_DONE )
1,417✔
NEW
290
    throwSqliteError( mDb->get(), "Failed to get list of columns for table " + tableName );
×
291

292
  if ( tableExists( mDb, "gpkg_geometry_columns", dbName ) )
2,834✔
293
  {
294
    int srsId = -1;
1,174✔
295
    Sqlite3Stmt stmtGeomCol;
1,174✔
296
    stmtGeomCol.prepare( mDb, "SELECT * FROM \"%w\".gpkg_geometry_columns WHERE table_name = '%q'", dbName.c_str(), tableName.c_str() );
1,174✔
297
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmtGeomCol.get() ) ) )
2,249✔
298
    {
299
      const unsigned char *chrColumnName = sqlite3_column_text( stmtGeomCol.get(), 1 );
1,075✔
300
      const unsigned char *chrTypeName = sqlite3_column_text( stmtGeomCol.get(), 2 );
1,075✔
301
      if ( chrColumnName == nullptr )
1,075✔
302
        throw GeoDiffException( "NULL column name in gpkg_geometry_columns: " + tableName );
×
303
      if ( chrTypeName == nullptr )
1,075✔
304
        throw GeoDiffException( "NULL type name in gpkg_geometry_columns: " + tableName );
×
305

306
      std::string geomColName = reinterpret_cast<const char *>( chrColumnName );
2,150✔
307
      std::string geomTypeName = reinterpret_cast<const char *>( chrTypeName );
1,075✔
308
      srsId = sqlite3_column_int( stmtGeomCol.get(), 3 );
1,075✔
309
      bool hasZ = sqlite3_column_int( stmtGeomCol.get(), 4 );
1,075✔
310
      bool hasM = sqlite3_column_int( stmtGeomCol.get(), 5 );
1,075✔
311

312
      bool found = false;
1,075✔
313
      for ( SqliteColumnInfo &def : defs )
2,151✔
314
      {
315
        if ( def.column.name == geomColName )
2,151✔
316
        {
317
          def.column.setGeometry( geomTypeName, srsId, hasM, hasZ );
1,075✔
318
          found = true;
1,075✔
319
          break;
1,075✔
320
        }
321
      }
322
      if ( !found )
1,075✔
UNCOV
323
        throw GeoDiffException( "Inconsistent entry in gpkg_geometry_columns - geometry column not found: " + geomColName );
×
324
    }
1,075✔
325
    if ( rc != SQLITE_DONE )
1,174✔
326
    {
327
      logSqliteError( context(), mDb, "Failed to get geometry column info for table " + tableName );
×
328
    }
329

330
    if ( srsId != -1 )
1,174✔
331
    {
332
      Sqlite3Stmt stmtCrs;
779✔
333
      stmtCrs.prepare( mDb, "SELECT * FROM \"%w\".gpkg_spatial_ref_sys WHERE srs_id = %d", dbName.c_str(), srsId );
779✔
334
      if ( SQLITE_ROW != sqlite3_step( stmtCrs.get() ) )
779✔
335
      {
336
        throwSqliteError( mDb->get(), "Unable to find entry in gpkg_spatial_ref_sys for srs_id = " + std::to_string( srsId ) );
×
337
      }
338

339
      const unsigned char *chrAuthName = sqlite3_column_text( stmtCrs.get(), 2 );
779✔
340
      const unsigned char *chrWkt = sqlite3_column_text( stmtCrs.get(), 4 );
779✔
341
      if ( chrAuthName == nullptr )
779✔
342
        throw GeoDiffException( "NULL auth name in gpkg_spatial_ref_sys: " + tableName );
×
343
      if ( chrWkt == nullptr )
779✔
344
        throw GeoDiffException( "NULL definition in gpkg_spatial_ref_sys: " + tableName );
×
345

346
      crs.srsId = srsId;
779✔
347
      crs.authName = reinterpret_cast<const char *>( chrAuthName );
779✔
348
      crs.authCode = sqlite3_column_int( stmtCrs.get(), 3 );
779✔
349
      crs.wkt = reinterpret_cast<const char *>( chrWkt );
779✔
350
    }
779✔
351
  }
1,174✔
352

353
  return {defs, crs};
2,834✔
354
}
1,417✔
355

356
TableSchema SqliteDriver::tableSchema( const std::string &tableName,
1,397✔
357
                                       bool useModified )
358
{
359
  std::string dbName = databaseName( useModified );
1,397✔
360

361
  if ( !tableExists( mDb, tableName, dbName ) )
1,397✔
362
    throw GeoDiffException( "Table does not exist: " + tableName );
2✔
363

364
  TableSchema tbl;
1,395✔
365
  tbl.name = tableName;
1,395✔
366

367
  const auto [defs, crs] = sqliteColumns( dbName, tableName );
1,395✔
368
  tbl.crs = crs;
1,395✔
369
  for ( const SqliteColumnInfo &def : defs )
6,396✔
370
  {
371
    if ( def.hidden )
5,001✔
NEW
372
      continue;
×
373

374
    tbl.columns.push_back( def.column );
5,001✔
375
  }
376

377
  // The declared type only maps to a base type once we know whether the column
378
  // holds geometries, which we have just found out above.
379
  for ( TableColumnInfo &col : tbl.columns )
6,396✔
380
  {
381
    const std::string declaredType = col.type.dbType;
5,001✔
382
    col.type = columnType( context(), declaredType, Driver::SQLITEDRIVERNAME, col.isGeometry );
5,001✔
383

384
    if ( col.isPrimaryKey && ( lowercaseString( col.type.dbType ) == "integer" ) )
5,001✔
385
    {
386
      // sqlite uses auto-increment automatically for INTEGER PRIMARY KEY - https://sqlite.org/autoinc.html
387
      col.isAutoIncrement = true;
1,380✔
388
    }
389
  }
5,001✔
390

391
  return tbl;
2,790✔
392
}
1,397✔
393

394
DatabaseSchema SqliteDriver::getSchema( bool useModified )
704✔
395
{
396
  std::vector<TableSchema> tables;
704✔
397
  for ( const std::string &name : listTables( useModified ) )
1,652✔
398
  {
399
    tables.push_back( tableSchema( name, useModified ) );
948✔
400
  }
704✔
401
  return {tables};
1,408✔
402
}
704✔
403

404
/**
405
 * printf() with sqlite extensions - see https://www.sqlite.org/printf.html
406
 * for extra format options like %q or %Q
407
 */
408
static std::string sqlitePrintf( const char *zFormat, ... )
15,299✔
409
{
410
  va_list ap;
411
  va_start( ap, zFormat );
15,299✔
412
  char *zSql = sqlite3_vmprintf( zFormat, ap );
15,299✔
413
  va_end( ap );
15,299✔
414

415
  if ( zSql == nullptr )
15,299✔
416
  {
417
    throw GeoDiffException( "out of memory" );
×
418
  }
419
  std::string res = reinterpret_cast<const char *>( zSql );
15,299✔
420
  sqlite3_free( zSql );
15,299✔
421
  return res;
30,598✔
422
}
×
423

424
struct TableDiffContext
425
{
426
  std::shared_ptr<Sqlite3Db> db;
427
  const TableSchema &schemaBase;
428
  const TableSchema &schemaModified;
429
  std::vector<TableColumnInfo> commonColumns;
430
  std::vector<TableColumnInfo> newColumns;
431
  ChangesetWriter &writer;
432
  bool tableEntryWritten = false;
433
};
434

435
static std::string sqlColumnsStr( const TableDiffContext &diffContext, bool reverse )
1,844✔
436
{
437
  const char *tableName = ( reverse ? diffContext.schemaBase.name : diffContext.schemaModified.name ).c_str();
1,844✔
438

439
  std::string colsStr; // Column list equivalent to modified schema
1,844✔
440
  for ( const TableColumnInfo &c : diffContext.schemaModified.columns )
8,488✔
441
  {
442
    if ( !colsStr.empty() )
6,644✔
443
      colsStr += ", ";
4,800✔
444
    if ( reverse )
6,644✔
445
    {
446
      // Check if this column also exists in base and NULL it out if not
447
      bool found = false;
3,322✔
448
      for ( const auto &commonCol : diffContext.commonColumns )
7,902✔
449
      {
450
        if ( commonCol.name == c.name )
7,852✔
451
        {
452
          found = true;
3,272✔
453
          break;
3,272✔
454
        }
455
      }
456
      if ( !found )
3,322✔
457
      {
458
        colsStr += sqlitePrintf( "NULL AS \"%w\"", c.name.c_str() );
50✔
459
        continue;
50✔
460
      }
461
    }
462
    colsStr += sqlitePrintf( "\"%w\".\"%w\".\"%w\"",
13,188✔
463
                             reverse ? "main" : "modified", tableName, c.name.c_str() );
6,594✔
464
  }
465
  return colsStr;
1,844✔
NEW
466
}
×
467

468
//! Constructs SQL query to get all rows that do not exist in the other table (used for insert and delete)
469
static std::string sqlFindInserted( const TableDiffContext &diffContext, bool reverse )
922✔
470
{
471
  const char *baseTableName = diffContext.schemaBase.name.c_str();
922✔
472
  const char *modifiedTableName = diffContext.schemaModified.name.c_str();
922✔
473

474
  std::string exprPk; // Filter expression checking primary key is equal
922✔
475
  for ( const TableColumnInfo &c : diffContext.commonColumns )
4,194✔
476
  {
477
    if ( c.isPrimaryKey )
3,272✔
478
    {
479
      if ( !exprPk.empty() )
930✔
480
        exprPk += " AND ";
8✔
481
      exprPk += sqlitePrintf( "\"modified\".\"%w\".\"%w\"=\"main\".\"%w\".\"%w\"",
1,860✔
482
                              modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
930✔
483
    }
484
  }
485

486
  std::string sql = sqlitePrintf( "SELECT %s FROM \"%w\".\"%w\" WHERE NOT EXISTS ( SELECT 1 FROM \"%w\".\"%w\" WHERE %s)",
487
                                  sqlColumnsStr( diffContext, reverse ).c_str(),
1,844✔
488
                                  reverse ? "main" : "modified", reverse ? baseTableName : modifiedTableName,
489
                                  reverse ? "modified" : "main", reverse ? modifiedTableName : baseTableName, exprPk.c_str() );
1,844✔
490
  return sql;
1,844✔
491
}
922✔
492

493
//! Constructs SQL query to get all modified rows for a single table
494
static std::string sqlFindModified( const TableDiffContext &diffContext )
461✔
495
{
496
  const char *baseTableName = diffContext.schemaBase.name.c_str();
461✔
497
  const char *modifiedTableName = diffContext.schemaModified.name.c_str();
461✔
498

499
  std::string exprPk;
461✔
500
  std::string exprOther;
461✔
501
  for ( const TableColumnInfo &c : diffContext.commonColumns )
2,097✔
502
  {
503
    if ( c.isPrimaryKey )
1,636✔
504
    {
505
      if ( !exprPk.empty() )
465✔
506
        exprPk += " AND ";
4✔
507
      exprPk += sqlitePrintf( "\"modified\".\"%w\".\"%w\"=\"main\".\"%w\".\"%w\"",
930✔
508
                              modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
465✔
509
    }
510
    else // not a primary key column
511
    {
512
      if ( !exprOther.empty() )
1,171✔
513
        exprOther += " OR ";
711✔
514

515
      exprOther += sqlitePrintf( "\"modified\".\"%w\".\"%w\" IS NOT \"main\".\"%w\".\"%w\"",
2,342✔
516
                                 modifiedTableName, c.name.c_str(), baseTableName, c.name.c_str() );
1,171✔
517
    }
518
  }
519

520
  // Check for non-NULL values in newly-added columns
521
  for ( const TableColumnInfo &c : diffContext.newColumns )
486✔
522
  {
523
    if ( !exprOther.empty() )
25✔
524
      exprOther += " OR ";
25✔
525

526
    exprOther += sqlitePrintf( "\"modified\".\"%w\".\"%w\" IS NOT NULL",
50✔
527
                               modifiedTableName, c.name.c_str() );
25✔
528
  }
529

530
  std::string colsStr = sqlColumnsStr( diffContext, false ) + ", " + sqlColumnsStr( diffContext, true );
461✔
531

532
  if ( exprOther.empty() )
461✔
533
  {
534
    return sqlitePrintf( "SELECT %s FROM \"modified\".\"%w\", \"main\".\"%w\" WHERE %s",
535
                         colsStr.c_str(), modifiedTableName, baseTableName, exprPk.c_str() );
1✔
536
  }
537
  else
538
  {
539
    return sqlitePrintf( "SELECT %s FROM \"modified\".\"%w\", \"main\".\"%w\" WHERE %s AND (%s)",
540
                         colsStr.c_str(), modifiedTableName, baseTableName, exprPk.c_str(), exprOther.c_str() );
460✔
541
  }
542
}
461✔
543

544

545
static Value changesetValue( sqlite3_value *v )
1,845✔
546
{
547
  Value x;
1,845✔
548
  int type = sqlite3_value_type( v );
1,845✔
549
  if ( type == SQLITE_NULL )
1,845✔
550
    x.setNull();
181✔
551
  else if ( type == SQLITE_INTEGER )
1,664✔
552
    x.setInt( sqlite3_value_int64( v ) );
846✔
553
  else if ( type == SQLITE_FLOAT )
818✔
554
    x.setDouble( sqlite3_value_double( v ) );
16✔
555
  else if ( type == SQLITE_TEXT )
802✔
556
    x.setString( Value::TypeText, reinterpret_cast<const char *>( sqlite3_value_text( v ) ), sqlite3_value_bytes( v ) );
527✔
557
  else if ( type == SQLITE_BLOB )
275✔
558
    x.setString( Value::TypeBlob, reinterpret_cast<const char *>( sqlite3_value_blob( v ) ), sqlite3_value_bytes( v ) );
275✔
559
  else
560
    throw GeoDiffException( "Unexpected value type" );
×
561

562
  return x;
1,845✔
563
}
×
564

565
static void handleInserted( const Context *context, TableDiffContext &diffContext, bool reverse )
922✔
566
{
567
  std::string sqlInserted = sqlFindInserted( diffContext, reverse );
922✔
568
  Sqlite3Stmt statementI;
922✔
569
  statementI.prepare( diffContext.db, "%s", sqlInserted.c_str() );
922✔
570
  int rc;
571
  while ( SQLITE_ROW == ( rc = sqlite3_step( statementI.get() ) ) )
1,242✔
572
  {
573
    if ( !diffContext.tableEntryWritten )
320✔
574
    {
575
      ChangesetTable chTable = schemaToChangesetTable( diffContext.schemaModified.name, diffContext.schemaModified );
191✔
576
      diffContext.writer.beginTable( chTable );
191✔
577
      diffContext.tableEntryWritten = true;
191✔
578
    }
191✔
579

580
    ChangesetDataEntry e;
320✔
581
    e.op = reverse ? ChangesetDataEntry::OpDelete : ChangesetDataEntry::OpInsert;
320✔
582

583
    size_t numColumns = diffContext.schemaModified.columns.size();
320✔
584
    for ( size_t i = 0; i < numColumns; ++i )
1,443✔
585
    {
586
      Sqlite3Value v( sqlite3_column_value( statementI.get(), static_cast<int>( i ) ) );
1,123✔
587
      if ( reverse )
1,123✔
588
        e.oldValues.push_back( changesetValue( v.value() ) );
151✔
589
      else
590
        e.newValues.push_back( changesetValue( v.value() ) );
972✔
591
    }
1,123✔
592

593
    diffContext.writer.writeEntry( e );
320✔
594
  }
320✔
595
  if ( rc != SQLITE_DONE )
922✔
596
  {
NEW
597
    logSqliteError( context, diffContext.db, "Failed to write information about inserted rows in table " + diffContext.schemaModified.name );
×
598
  }
599
}
922✔
600

601
static void handleUpdated( const Context *context, TableDiffContext &diffContext )
461✔
602
{
603
  std::string sqlModified = sqlFindModified( diffContext );
461✔
604

605
  Sqlite3Stmt statement;
461✔
606
  statement.prepare( diffContext.db, "%s", sqlModified.c_str() );
461✔
607
  int rc;
608
  while ( SQLITE_ROW == ( rc = sqlite3_step( statement.get() ) ) )
563✔
609
  {
610
    /*
611
    ** Within the old.* record associated with an UPDATE change, all fields
612
    ** associated with table columns that are not PRIMARY KEY columns and are
613
    ** not modified by the UPDATE change are set to "undefined". Other fields
614
    ** are set to the values that made up the row before the UPDATE that the
615
    ** change records took place. Within the new.* record, fields associated
616
    ** with table columns modified by the UPDATE change contain the new
617
    ** values. Fields associated with table columns that are not modified
618
    ** are set to "undefined".
619
    */
620

621
    ChangesetDataEntry e;
102✔
622
    e.op = ChangesetDataEntry::OpUpdate;
102✔
623

624
    bool hasUpdates = false;
102✔
625
    size_t numColumns = diffContext.schemaModified.columns.size();
102✔
626
    for ( size_t i = 0; i < numColumns; ++i )
495✔
627
    {
628
      Sqlite3Value v1( sqlite3_column_value( statement.get(), static_cast<int>( i + numColumns ) ) );
393✔
629
      Sqlite3Value v2( sqlite3_column_value( statement.get(), static_cast<int>( i ) ) );
393✔
630
      bool pkey = diffContext.schemaModified.columns[i].isPrimaryKey;
393✔
631
      bool updated = ( v1 != v2 );
393✔
632
      if ( updated )
393✔
633
      {
634
        // Let's do a secondary check for some column types to avoid false positives, for example
635
        // multiple different string representations could be used for a single datetime value,
636
        // see "Time Values" section in https://sqlite.org/lang_datefunc.html
637
        // Use strftime() to take into account fractional seconds
638
        if ( diffContext.schemaModified.columns[i].type == TableColumnType::DATETIME )
115✔
639
        {
640
          Sqlite3Stmt stmtDatetime;
8✔
641
          stmtDatetime.prepare( diffContext.db, "SELECT STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?1) IS NOT STRFTIME('%%Y-%%m-%%d %%H:%%M:%%f', ?2)" );
8✔
642
          sqlite3_bind_value( stmtDatetime.get(), 1, v1.value() );
8✔
643
          sqlite3_bind_value( stmtDatetime.get(), 2, v2.value() );
8✔
644
          int res = sqlite3_step( stmtDatetime.get() );
8✔
645
          if ( SQLITE_ROW == res )
8✔
646
          {
647
            updated = sqlite3_column_int( stmtDatetime.get(), 0 );
8✔
648
          }
649
          else if ( SQLITE_DONE != res )
×
650
          {
NEW
651
            logSqliteError( context, diffContext.db, "Failed to write information about updated rows in table " + diffContext.schemaModified.name );
×
652
          }
653
        }
8✔
654

655
        if ( updated )
115✔
656
        {
657
          hasUpdates = true;
113✔
658
        }
659
      }
660
      e.oldValues.push_back( ( pkey || updated ) ? changesetValue( v1.value() ) : Value() );
608✔
661
      e.newValues.push_back( updated ? changesetValue( v2.value() ) : Value() );
393✔
662
    }
393✔
663

664
    if ( hasUpdates )
102✔
665
    {
666
      if ( !diffContext.tableEntryWritten )
100✔
667
      {
668
        ChangesetTable chTable = schemaToChangesetTable( diffContext.schemaModified.name, diffContext.schemaModified );
52✔
669
        diffContext.writer.beginTable( chTable );
52✔
670
        diffContext.tableEntryWritten = true;
52✔
671
      }
52✔
672

673
      diffContext.writer.writeEntry( e );
100✔
674
    }
675
  }
102✔
676
  if ( rc != SQLITE_DONE )
461✔
677
  {
NEW
678
    logSqliteError( context, diffContext.db, "Failed to write information about inserted rows in table " + diffContext.schemaModified.name );
×
679
  }
680
}
461✔
681

682
// To allow diff inversion to work, we first delete all rows when dropping a
683
// table, and NULL out all rows when dropping a column.
684
static void writeDataChangesForSchemaChange( std::shared_ptr<Sqlite3Db> db, const std::unordered_map<std::string, TableSchema> &currentSchemata, ChangesetWriter &writer, const ChangesetEntry &entry )
53✔
685
{
686
  if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
53✔
687
  {
688
    auto it = currentSchemata.find( dcEntry->tableName );
14✔
689
    if ( it == currentSchemata.end() )
14✔
NEW
690
      throw GeoDiffException( "Missing schema for table " + dcEntry->tableName );
×
691
    const TableSchema &table = it->second;
14✔
692

693
    size_t droppedColIdx = table.columnFromName( dcEntry->column.name );
14✔
694
    if ( droppedColIdx == SIZE_MAX )
14✔
NEW
695
      throw GeoDiffException( "Could not find column " + dcEntry->column.name + " to delete" );
×
696

697
    std::string pkeyColStr;
14✔
698
    for ( const TableColumnInfo &c : table.columns )
66✔
699
    {
700
      if ( c.isPrimaryKey )
52✔
701
      {
702
        if ( !pkeyColStr.empty() )
14✔
NEW
703
          pkeyColStr += ", ";
×
704
        pkeyColStr += sqlitePrintf( "\"%w\"", c.name.c_str() );
14✔
705
      }
706
    }
707
    if ( pkeyColStr.empty() )
14✔
NEW
708
      throw GeoDiffException( "Table " + table.name + " has no primary key" );
×
709

710
    Sqlite3Stmt stmt;
14✔
711
    stmt.prepare( db, "SELECT %s, \"%w\" FROM \"main\".\"%w\" WHERE \"%w\" IS NOT NULL",
14✔
712
                  pkeyColStr.c_str(), dcEntry->column.name.c_str(), dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
713

714
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
14✔
715
    int rc;
716
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
55✔
717
    {
718
      ChangesetDataEntry e;
41✔
719
      e.op = ChangesetDataEntry::OpUpdate;
41✔
720

721
      size_t idxInResult = 0;
41✔
722
      for ( size_t i = 0; i < table.columns.size(); ++i )
192✔
723
      {
724
        bool isPkey = table.columns[i].isPrimaryKey;
151✔
725
        bool isDroppedCol = i == droppedColIdx;
151✔
726

727
        if ( isPkey || isDroppedCol )
151✔
728
        {
729
          Sqlite3Value v( sqlite3_column_value( stmt.get(), static_cast<int>( idxInResult ) ) );
82✔
730
          e.oldValues.push_back( changesetValue( v.value() ) );
82✔
731
          idxInResult++;
82✔
732
        }
82✔
733
        else
734
          e.oldValues.push_back( Value() );
69✔
735

736
        if ( isDroppedCol )
151✔
737
        {
738
          Value nullVal;
41✔
739
          nullVal.setNull();
41✔
740
          e.newValues.push_back( nullVal );
41✔
741
        }
41✔
742
        else
743
          e.newValues.push_back( Value() );
110✔
744
      }
745

746
      writer.writeEntry( e );
41✔
747
    }
41✔
748
  }
14✔
749
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
39✔
750
  {
751
    auto it = currentSchemata.find( dtEntry->tableName );
6✔
752
    if ( it == currentSchemata.end() )
6✔
NEW
753
      throw GeoDiffException( "Missing schema for table " + dtEntry->tableName );
×
754
    const TableSchema &table = it->second;
6✔
755

756
    Sqlite3Stmt stmt;
6✔
757
    stmt.prepare( db, "SELECT * FROM \"main\".\"%w\"", dtEntry->tableName.c_str() );
6✔
758

759
    writer.beginTable( schemaToChangesetTable( table.name, table ) );
6✔
760
    int rc;
761
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt.get() ) ) )
21✔
762
    {
763
      ChangesetDataEntry e;
15✔
764
      e.op = ChangesetDataEntry::OpDelete;
15✔
765

766
      size_t numColumns = table.columns.size();
15✔
767
      for ( size_t i = 0; i < numColumns; ++i )
60✔
768
      {
769
        Sqlite3Value v( sqlite3_column_value( stmt.get(), static_cast<int>( i ) ) );
45✔
770
        e.oldValues.push_back( changesetValue( v.value() ) );
45✔
771
      }
45✔
772

773
      writer.writeEntry( e );
15✔
774
    }
15✔
775
  }
6✔
776
}
53✔
777

778
void SqliteDriver::createChangeset( ChangesetWriter &writer )
352✔
779
{
780
  DatabaseSchema schemaBase = getSchema( false );
352✔
781
  DatabaseSchema schemaModified = getSchema( true );
352✔
782

783
  // We keep table schemata that have exactly the written out schema-change
784
  // entries applied. They're necessary to know the intermediate database state
785
  // for any data changes (e.g. row deletions before table drop).
786
  std::unordered_map<std::string, TableSchema> currentSchemata;
352✔
787
  for ( const TableSchema &tbl : schemaBase.tables )
825✔
788
    currentSchemata[tbl.name] = tbl;
473✔
789

790
  auto schemaDiffEntries = diffDatabaseSchema( schemaBase, schemaModified );
352✔
791
  for ( const ChangesetEntry &entry : schemaDiffEntries )
402✔
792
  {
793
    writeDataChangesForSchemaChange( mDb, currentSchemata, writer, entry );
53✔
794
    writer.writeEntry( entry );
53✔
795

796
    if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
53✔
797
      simulateColumnChange( currentSchemata[acEntry->tableName], entry );
25✔
798
    else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
28✔
799
      simulateColumnChange( currentSchemata[dcEntry->tableName], entry );
14✔
800
  }
801

802
  for ( const TableSchema &tblModified : schemaModified.tables )
821✔
803
  {
804
    if ( !tblModified.hasPrimaryKey() )
472✔
805
      continue;  // ignore tables without primary key - they can't be compared properly
11✔
806

807
    // Find corresponding table in base DB
808
    const TableSchema *tblBase = nullptr;
465✔
809
    for ( const TableSchema &tbl : schemaBase.tables )
628✔
810
    {
811
      if ( tbl.name == tblModified.name )
624✔
812
      {
813
        tblBase = &tbl;
461✔
814
        break;
461✔
815
      }
816
    }
817

818
    if ( !tblBase )
465✔
819
    {
820
      // Table was newly added, just dump data using INSERTs
821
      dumpTableData( writer, tblModified, true );
4✔
822
      continue;
4✔
823
    }
824

825
    TableDiffContext diffContext = { mDb, *tblBase, tblModified, {}, {}, writer };
461✔
826

827
    for ( const TableColumnInfo &baseColumn : tblBase->columns )
2,111✔
828
    {
829
      for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
3,905✔
830
      {
831
        if ( baseColumn.name == modifiedColumn.name )
3,891✔
832
        {
833
          diffContext.commonColumns.push_back( modifiedColumn );
1,636✔
834
          break;
1,636✔
835
        }
836
      }
837
    }
838

839
    for ( const TableColumnInfo &modifiedColumn : tblModified.columns )
2,122✔
840
    {
841
      bool found = false;
1,661✔
842
      for ( const TableColumnInfo &baseColumn : tblBase->columns )
3,957✔
843
      {
844
        if ( baseColumn.name == modifiedColumn.name )
3,932✔
845
        {
846
          found = true;
1,636✔
847
          break;
1,636✔
848
        }
849
      }
850
      if ( !found )
1,661✔
851
        diffContext.newColumns.push_back( modifiedColumn );
25✔
852
    }
853

854
    handleInserted( context(), diffContext, false );  // INSERT
461✔
855
    handleInserted( context(), diffContext, true );   // DELETE
461✔
856
    handleUpdated( context(), diffContext );          // UPDATE
461✔
857
  }
461✔
858
}
358✔
859

860
static std::string sqlForInsert( const std::string &tableName, const TableSchema &tbl )
229✔
861
{
862
  /*
863
   * For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
864
   *
865
   * INSERT INTO x (a, b, c, d) VALUES (?, ?, ?, ?)
866
   */
867

868
  std::string sql;
229✔
869
  sql += sqlitePrintf( "INSERT INTO \"%w\" (", tableName.c_str() );
229✔
870
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
1,037✔
871
  {
872
    if ( i > 0 )
808✔
873
      sql += ", ";
579✔
874
    sql += sqlitePrintf( "\"%w\"", tbl.columns[i].name.c_str() );
808✔
875
  }
876
  sql += ") VALUES (";
229✔
877
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
1,037✔
878
  {
879
    if ( i > 0 )
808✔
880
      sql += ", ";
579✔
881
    sql += "?";
808✔
882
  }
883
  sql += ")";
229✔
884
  return sql;
229✔
885
}
×
886

887
static std::string sqlForUpdate( const std::string &tableName, const TableSchema &tbl )
229✔
888
{
889
  /*
890
  ** For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
891
  **
892
  **     UPDATE x SET
893
  **     a = CASE WHEN ?2  THEN ?3  ELSE a END,
894
  **     b = CASE WHEN ?5  THEN ?6  ELSE b END,
895
  **     c = CASE WHEN ?8  THEN ?9  ELSE c END,
896
  **     d = CASE WHEN ?11 THEN ?12 ELSE d END
897
  **     WHERE a = ?1 AND c = ?7 AND (?13 OR
898
  **       (?5==0 OR b IS ?4) AND (?11==0 OR d IS ?10) AND
899
  **     )
900
  **
901
  ** For each column in the table, there are three variables to bind:
902
  **
903
  **     ?(i*3+1)    The old.* value of the column, if any.
904
  **     ?(i*3+2)    A boolean flag indicating that the value is being modified.
905
  **     ?(i*3+3)    The new.* value of the column, if any.
906
  */
907

908
  std::string sql;
229✔
909
  sql += sqlitePrintf( "UPDATE \"%w\" SET ", tableName.c_str() );
229✔
910

911
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
1,037✔
912
  {
913
    if ( i > 0 )
808✔
914
      sql += ", ";
579✔
915
    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() );
808✔
916
  }
917
  sql += " WHERE ";
229✔
918
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
1,037✔
919
  {
920
    if ( i > 0 )
808✔
921
      sql += " AND ";
579✔
922
    if ( tbl.columns[i].isPrimaryKey )
808✔
923
      sql += sqlitePrintf( " \"%w\" = ?%d ", tbl.columns[i].name.c_str(), i * 3 + 1 );
229✔
924
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
579✔
925
    {
926
      // compare date/time values using datetime() because they may have
927
      // multiple equivalent string representations (see #143)
928
      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✔
929
    }
930
    else
931
      sql += sqlitePrintf( " ( ?%d = 0 OR \"%w\" IS ?%d ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 );
575✔
932
  }
933

934
  return sql;
229✔
935
}
×
936

937
static std::string sqlForDelete( const std::string &tableName, const TableSchema &tbl )
229✔
938
{
939
  /*
940
   * For a table defined like this: CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
941
   *
942
   * DELETE FROM x WHERE a = ? AND b IS ? AND c = ? AND d IS ?
943
   */
944

945
  std::string sql;
229✔
946
  sql += sqlitePrintf( "DELETE FROM \"%w\" WHERE ", tableName.c_str() );
229✔
947
  for ( size_t i = 0; i < tbl.columns.size(); ++i )
1,037✔
948
  {
949
    if ( i > 0 )
808✔
950
      sql += " AND ";
579✔
951
    if ( tbl.columns[i].isPrimaryKey )
808✔
952
      sql += sqlitePrintf( "\"%w\" = ?", tbl.columns[i].name.c_str() );
229✔
953
    else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME )
579✔
954
    {
955
      // compare date/time values using strftime() because otherwise
956
      // fractional seconds will be lost
957
      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✔
958
    }
959
    else
960
      sql += sqlitePrintf( "\"%w\" IS ?", tbl.columns[i].name.c_str() );
575✔
961
  }
962
  return sql;
229✔
963
}
×
964

965
static void bindValue( sqlite3_stmt *stmt, int index, const Value &v )
1,535✔
966
{
967
  int rc;
968
  if ( v.type() == Value::TypeInt )
1,535✔
969
    rc = sqlite3_bind_int64( stmt, index, v.getInt() );
664✔
970
  else if ( v.type() == Value::TypeDouble )
871✔
971
    rc = sqlite3_bind_double( stmt, index, v.getDouble() );
12✔
972
  else if ( v.type() == Value::TypeNull )
859✔
973
    rc = sqlite3_bind_null( stmt, index );
194✔
974
  else if ( v.type() == Value::TypeText )
665✔
975
    rc = sqlite3_bind_text( stmt, index, v.getString().c_str(), -1, SQLITE_TRANSIENT );
448✔
976
  else if ( v.type() == Value::TypeBlob )
217✔
977
    rc = sqlite3_bind_blob( stmt, index, v.getString().c_str(), ( int ) v.getString().size(), SQLITE_TRANSIENT );
217✔
978
  else
979
    throw GeoDiffException( "unexpected bind type" );
×
980

981
  if ( rc != SQLITE_OK )
1,535✔
982
  {
983
    throw GeoDiffException( "bind failed" );
×
984
  }
985
}
1,535✔
986

987

988
ChangeApplyResult SqliteDriver::applyDataChange( SqliteChangeApplyState &state, const ChangesetDataEntry &entry )
440✔
989
{
990
  std::string tableName = entry.table->name;
440✔
991

992
  if ( startsWith( tableName, "gpkg_" ) ) // skip any changes to GPKG meta tables
880✔
993
    return ChangeApplyResult::Skipped;
4✔
994

995
  if ( context()->isTableSkipped( tableName ) ) // skip table if necessary
436✔
996
    return ChangeApplyResult::Skipped;
10✔
997

998
  if ( state.tableState.count( entry.table ) == 0 )
426✔
999
  {
1000
    TableSchema schema = tableSchema( tableName );
229✔
1001

1002
    if ( schema.columns.size() == 0 )
229✔
1003
      throw GeoDiffException( "No such table: " + tableName );
×
1004

1005
    if ( schema.columns.size() != entry.table->columnCount() )
229✔
1006
      throw GeoDiffException( "Wrong number of columns for table: " + tableName );
×
1007

1008
    for ( size_t i = 0; i < entry.table->columnCount(); ++i )
1,037✔
1009
    {
1010
      if ( schema.columns[i].isPrimaryKey != entry.table->primaryKeys[i] )
808✔
1011
        throw GeoDiffException( "Mismatch of primary keys in table: " + tableName );
×
1012
    }
1013

1014
    SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table];
229✔
1015
    tbl.schema = schema;
229✔
1016

1017
    tbl.stmtInsert.prepare( mDb, sqlForInsert( tableName, schema ) );
229✔
1018
    tbl.stmtUpdate.prepare( mDb, sqlForUpdate( tableName, schema ) );
229✔
1019
    tbl.stmtDelete.prepare( mDb, sqlForDelete( tableName, schema ) );
229✔
1020
  }
229✔
1021
  SqliteChangeApplyState::TableState &tbl = state.tableState[entry.table];
426✔
1022

1023
  if ( entry.op == SQLITE_INSERT )
426✔
1024
  {
1025
    sqlite3_reset( tbl.stmtInsert.get() );
197✔
1026
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
902✔
1027
    {
1028
      const Value &v = entry.newValues[i];
705✔
1029
      bindValue( tbl.stmtInsert.get(), static_cast<int>( i ) + 1, v );
705✔
1030
    }
1031
    int res = sqlite3_step( tbl.stmtInsert.get() );
197✔
1032
    if ( res == SQLITE_CONSTRAINT )
197✔
1033
      return ChangeApplyResult::ConstraintConflict;
4✔
1034
    else if ( res != SQLITE_DONE )
193✔
1035
    {
1036
      logApplyConflict( "insert_failed", entry, true );
×
1037
      throw GeoDiffException( "SQLite error in INSERT" );
×
1038
    }
1039
    else if ( sqlite3_changes( mDb->get() ) != 1 )
193✔
1040
      throw GeoDiffException( "Nothing inserted (this should never happen)" );
×
1041
  }
1042
  else if ( entry.op == SQLITE_UPDATE )
229✔
1043
  {
1044
    sqlite3_reset( tbl.stmtUpdate.get() );
148✔
1045
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
701✔
1046
    {
1047
      const Value &vOld = entry.oldValues[i];
553✔
1048
      const Value &vNew = entry.newValues[i];
553✔
1049
      sqlite3_bind_int( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 2, vNew.type() != Value::TypeUndefined );
553✔
1050
      if ( vOld.type() != Value::TypeUndefined )
553✔
1051
        bindValue( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 1, vOld );
344✔
1052
      if ( vNew.type() != Value::TypeUndefined )
553✔
1053
        bindValue( tbl.stmtUpdate.get(), static_cast<int>( i ) * 3 + 3, vNew );
196✔
1054
    }
1055
    int res = sqlite3_step( tbl.stmtUpdate.get() );
148✔
1056
    if ( res == SQLITE_CONSTRAINT )
148✔
1057
      return ChangeApplyResult::ConstraintConflict;
×
1058
    else if ( res != SQLITE_DONE )
148✔
1059
    {
1060
      logApplyConflict( "update_failed", entry, true );
×
1061
      throw GeoDiffException( "SQLite error in UPDATE" );
×
1062
    }
1063
    else if ( sqlite3_changes( mDb->get() ) == 0 )
148✔
1064
    {
1065
      // either the row with such pkey does not exist or its data have been modified
1066
      logApplyConflict( "update_nothing", entry );
6✔
1067
      return ChangeApplyResult::NoChange;
2✔
1068
    }
1069
  }
1070
  else if ( entry.op == SQLITE_DELETE )
81✔
1071
  {
1072
    sqlite3_reset( tbl.stmtDelete.get() );
81✔
1073
    for ( size_t i = 0; i < tbl.schema.columns.size(); ++i )
371✔
1074
    {
1075
      const Value &v = entry.oldValues[i];
290✔
1076
      bindValue( tbl.stmtDelete.get(), static_cast<int>( i ) + 1, v );
290✔
1077
    }
1078
    int res = sqlite3_step( tbl.stmtDelete.get() );
81✔
1079
    if ( res == SQLITE_CONSTRAINT )
81✔
1080
      return ChangeApplyResult::ConstraintConflict;
×
1081
    else if ( res != SQLITE_DONE )
81✔
1082
    {
1083
      logApplyConflict( "delete_failed", entry, true );
×
1084
      throw GeoDiffException( "SQLite error in DELETE" );
×
1085
    }
1086
    else if ( sqlite3_changes( mDb->get() ) == 0 )
81✔
1087
    {
1088
      // either the row with such pkey does not exist or its data have been modified
1089
      logApplyConflict( "delete_nothing", entry );
3✔
1090
      return ChangeApplyResult::NoChange;
1✔
1091
    }
1092
  }
1093
  else
1094
    throw GeoDiffException( "Unexpected operation" );
×
1095

1096
  return ChangeApplyResult::Applied;
419✔
1097
}
440✔
1098

1099
static void addGpkgCrsDefinition( std::shared_ptr<Sqlite3Db> db, const CrsDefinition &crs )
134✔
1100
{
1101
  // gpkg_spatial_ref_sys
1102
  //   srs_name TEXT NOT NULL, srs_id INTEGER NOT NULL PRIMARY KEY,
1103
  //   organization TEXT NOT NULL, organization_coordsys_id INTEGER NOT NULL,
1104
  //   definition  TEXT NOT NULL, description TEXT
1105

1106
  Sqlite3Stmt stmtCheck;
134✔
1107
  stmtCheck.prepare( db, "select count(*) from gpkg_spatial_ref_sys where srs_id = %d;", crs.srsId );
134✔
1108
  int res = sqlite3_step( stmtCheck.get() );
134✔
1109
  if ( res != SQLITE_ROW )
134✔
1110
  {
1111
    throwSqliteError( db->get(), "Failed to access gpkg_spatial_ref_sys table" );
×
1112
  }
1113

1114
  if ( sqlite3_column_int( stmtCheck.get(), 0 ) )
134✔
1115
    return;  // already there
134✔
1116

NEW
1117
  if ( crs.wkt.size() == 0 )
×
NEW
1118
    throw GeoDiffException( "Tried to add new CRS without WKT definition" );
×
1119

1120
  Sqlite3Stmt stmt;
×
1121
  stmt.prepare( db, "INSERT INTO gpkg_spatial_ref_sys VALUES ('%q:%d', %d, '%q', %d, '%q', '')",
×
1122
                crs.authName.c_str(), crs.authCode, crs.srsId, crs.authName.c_str(), crs.authCode,
×
1123
                crs.wkt.c_str() );
1124
  res = sqlite3_step( stmt.get() );
×
1125
  if ( res != SQLITE_DONE )
×
1126
  {
1127
    throwSqliteError( db->get(), "Failed to insert CRS to gpkg_spatial_ref_sys table" );
×
1128
  }
1129
}
134✔
1130

1131
static void addGpkgSpatialTable( std::shared_ptr<Sqlite3Db> db, const TableSchema &tbl, const Extent &extent )
134✔
1132
{
1133
  size_t i = tbl.geometryColumn();
134✔
1134
  if ( i == SIZE_MAX )
134✔
1135
    throw GeoDiffException( "Adding non-spatial tables is not supported: " + tbl.name );
×
1136

1137
  const TableColumnInfo &col = tbl.columns[i];
134✔
1138
  std::string geomColumn = col.name;
134✔
1139
  std::string geomType = col.geomType;
134✔
1140
  int srsId = col.geomSrsId;
134✔
1141
  bool hasZ = col.geomHasZ;
134✔
1142
  bool hasM = col.geomHasM;
134✔
1143

1144
  // gpkg_contents
1145
  //   table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL,
1146
  //   identifier TEXT, description TEXT DEFAULT '',
1147
  //   last_change DATETIME NOT NULL DEFAULT (...),
1148
  //   min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE,
1149
  //   srs_id INTEGER
1150

1151
  Sqlite3Stmt stmt;
134✔
1152
  stmt.prepare( db, "INSERT INTO gpkg_contents (table_name, data_type, identifier, min_x, min_y, max_x, max_y, srs_id) "
134✔
1153
                "VALUES ('%q', 'features', '%q', %f, %f, %f, %f, %d)",
1154
                tbl.name.c_str(), tbl.name.c_str(), extent.minX, extent.minY, extent.maxX, extent.maxY, srsId );
134✔
1155
  int res = sqlite3_step( stmt.get() );
134✔
1156
  if ( res != SQLITE_DONE )
134✔
1157
  {
1158
    throwSqliteError( db->get(), "Failed to insert row to gpkg_contents table" );
×
1159
  }
1160

1161
  // gpkg_geometry_columns
1162
  //   table_name TEXT NOT NULL, column_name TEXT NOT NULL,
1163
  //   geometry_type_name TEXT NOT NULL, srs_id INTEGER NOT NULL,
1164
  //   z TINYINT NOT NULL,m TINYINT NOT NULL
1165

1166
  Sqlite3Stmt stmtGeomCol;
134✔
1167
  stmtGeomCol.prepare( db, "INSERT INTO gpkg_geometry_columns VALUES ('%q', '%q', '%q', %d, %d, %d)",
134✔
1168
                       tbl.name.c_str(), geomColumn.c_str(), geomType.c_str(), srsId, hasZ, hasM );
1169
  res = sqlite3_step( stmtGeomCol.get() );
134✔
1170
  if ( res != SQLITE_DONE )
134✔
1171
  {
1172
    throwSqliteError( db->get(), "Failed to insert row to gpkg_geometry_columns table" );
×
1173
  }
1174
}
134✔
1175

1176
static void createTable( std::shared_ptr<Sqlite3Db> db, const TableSchema &tbl )
137✔
1177
{
1178
  if ( tbl.geometryColumn() != SIZE_MAX )
137✔
1179
  {
1180
    addGpkgCrsDefinition( db, tbl.crs );
134✔
1181
    addGpkgSpatialTable( db, tbl, Extent() );   // TODO: is it OK to set zeros?
134✔
1182
  }
1183

1184
  std::string sql, pkeyCols, columns;
137✔
1185
  for ( const TableColumnInfo &c : tbl.columns )
558✔
1186
  {
1187
    if ( !columns.empty() )
421✔
1188
      columns += ", ";
284✔
1189

1190
    columns += sqlitePrintf( "\"%w\" %s", c.name.c_str(), c.type.dbType.c_str() );
421✔
1191

1192
    if ( c.isNotNull )
421✔
1193
      columns += " NOT NULL";
24✔
1194

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

1201
    if ( c.isPrimaryKey )
421✔
1202
    {
1203
      if ( !pkeyCols.empty() )
135✔
NEW
1204
        pkeyCols += ", ";
×
1205
      pkeyCols += sqlitePrintf( "\"%w\"", c.name.c_str() );
135✔
1206
    }
1207
  }
1208

1209
  sql = sqlitePrintf( "CREATE TABLE \"%w\" (", tbl.name.c_str() );
137✔
1210
  if ( !columns.empty() )
137✔
1211
  {
1212
    sql += columns;
137✔
1213
  }
1214
  if ( !pkeyCols.empty() )
137✔
1215
  {
1216
    sql += ", PRIMARY KEY (" + pkeyCols + ")";
135✔
1217
  }
1218
  sql += ");";
137✔
1219

1220
  Sqlite3Stmt stmt;
137✔
1221
  stmt.prepare( db, sql );
137✔
1222
  if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
137✔
1223
  {
NEW
1224
    throwSqliteError( db->get(), "Failure creating table: " + tbl.name );
×
1225
  }
1226
}
137✔
1227

1228
static void removeGpkgSpatialTable( std::shared_ptr<Sqlite3Db> db, const std::string &tableName )
4✔
1229
{
1230
  {
1231
    Sqlite3Stmt stmt;
4✔
1232
    stmt.prepare( db, "DELETE FROM gpkg_contents WHERE table_name = '%q'",
4✔
1233
                  tableName.c_str() );
1234
    int res = sqlite3_step( stmt.get() );
4✔
1235
    if ( res != SQLITE_DONE )
4✔
NEW
1236
      throwSqliteError( db->get(), "Failed to delete table from gpkg_contents table" );
×
1237
  }
4✔
1238

1239
  {
1240
    Sqlite3Stmt stmt;
4✔
1241
    stmt.prepare( db, "DELETE FROM gpkg_geometry_columns WHERE table_name = '%q'",
4✔
1242
                  tableName.c_str() );
1243
    int res = sqlite3_step( stmt.get() );
4✔
1244
    if ( res != SQLITE_DONE )
4✔
NEW
1245
      throwSqliteError( db->get(), "Failed to delete table from gpkg_geometry_columns table" );
×
1246
  }
4✔
1247
}
4✔
1248

1249
//! Returns wantedName, with a numeric suffix added if it is already taken
1250
static std::string unusedName( const std::vector<std::string> &usedNames, const std::string &wantedName )
6✔
1251
{
1252
  std::string name = wantedName;
6✔
1253
  int suffix = 0;
6✔
1254
  while ( std::find( usedNames.begin(), usedNames.end(), name ) != usedNames.end() )
6✔
NEW
1255
    name = wantedName + "_" + std::to_string( ++suffix );
×
1256
  return name;
6✔
NEW
1257
}
×
1258

1259
//! Builds the column definition for an ALTER TABLE ... ADD COLUMN of a copy of an existing column
1260
static std::string addColumnDefinition( const std::string &name, const SqliteColumnInfo &def )
6✔
1261
{
1262
  std::string sql = sqlitePrintf( "\"%w\" %s", name.c_str(), def.column.type.dbType.c_str() );
6✔
1263
  if ( def.column.isNotNull )
6✔
NEW
1264
    sql += " NOT NULL";
×
1265
  if ( !def.defaultValue.empty() )
6✔
NEW
1266
    sql += " DEFAULT " + def.defaultValue;
×
1267
  return sql;
6✔
NEW
1268
}
×
1269

1270
/**
1271
 * Adds a column to a table at a given index.
1272
 */
1273
void SqliteDriver::addColumnAtIndex( const TableSchema &table, size_t columnIdx, const TableColumnInfo &column )
22✔
1274
{
1275
  // SQLite's ADD COLUMN can only append, and there is no way to reorder
1276
  // columns, so to get the new column to columnIdx we append it and then for
1277
  // every column that should be to its right: append a copy of the column,
1278
  // copy the values over, drop the original, and rename the copies back.
1279
  if ( columnIdx > table.columns.size() )
22✔
NEW
1280
    throw GeoDiffException( "Tried to add column " + table.name + "." + column.name + " at index " +
×
NEW
1281
                            std::to_string( columnIdx ) + ", which is past the end of the table" );
×
1282

1283
  const std::string cannotAdd = "Cannot add column " + table.name + "." + column.name +
44✔
1284
                                " at index " + std::to_string( columnIdx ) + ": ";
66✔
1285

1286
  const auto [defs, unusedCrs] = sqliteColumns( databaseName(), table.name );
22✔
1287
  // Sanity checks for supported scenarios
1288
  for ( const SqliteColumnInfo &def : defs )
90✔
1289
  {
1290
    if ( def.hidden )
68✔
NEW
1291
      throw GeoDiffException( cannotAdd + "table has a generated or hidden column (" + def.column.name + ")" );
×
1292
  }
1293
  if ( defs.size() != table.columns.size() )
22✔
NEW
1294
    throw GeoDiffException( "SQLite reports " + std::to_string( defs.size() ) + " columns in table " + table.name +
×
NEW
1295
                            ", but we counted " + std::to_string( table.columns.size() ) );
×
1296
  for ( size_t i = columnIdx; i < defs.size(); ++i )
28✔
1297
  {
1298
    if ( defs[i].column.name != table.columns[i].name )
7✔
NEW
1299
      throw GeoDiffException( "SQLite reports column " + std::to_string( i ) + " of table " + table.name + " as " +
×
NEW
1300
                              defs[i].column.name + ", but we expect " + table.columns[i].name );
×
1301

1302
    // Throw on unsupported columns with a nicer error message than SQLite would give us.
1303
    const std::string wouldMove = cannotAdd + "column " + defs[i].column.name + " would have to be moved, but ";
7✔
1304
    if ( defs[i].column.isPrimaryKey )
7✔
NEW
1305
      throw GeoDiffException( wouldMove + "it is part of the primary key" );
×
1306
    if ( table.columns[i].isGeometry )
7✔
1307
      throw GeoDiffException( wouldMove + "it is a geometry column" );
1✔
1308
    if ( defs[i].column.isNotNull && defs[i].defaultValue.empty() )
6✔
NEW
1309
      throw GeoDiffException( wouldMove + "it is NOT NULL and has no default value" );
×
1310
  }
7✔
1311

1312
  executeSql( sqlitePrintf( "ALTER TABLE \"%w\" ADD COLUMN \"%w\" %s",
21✔
1313
                            table.name.c_str(), column.name.c_str(), column.type.dbType.c_str() ) );
1314
  if ( columnIdx == table.columns.size() )
21✔
1315
    return;
17✔
1316

1317
  // Append a copy of every column that has to move
1318
  std::vector<std::string> usedNames;
4✔
1319
  for ( const SqliteColumnInfo &def : defs )
18✔
1320
    usedNames.push_back( def.column.name );
14✔
1321
  usedNames.push_back( column.name );
4✔
1322

1323
  std::vector<std::string> copyNames;
4✔
1324
  for ( size_t i = columnIdx; i < defs.size(); ++i )
10✔
1325
  {
1326
    std::string copyName = unusedName( usedNames, "geodiff_moved_" + defs[i].column.name );
6✔
1327
    usedNames.push_back( copyName );
6✔
1328
    copyNames.push_back( copyName );
6✔
1329

1330
    executeSql( sqlitePrintf( "ALTER TABLE \"%w\" ADD COLUMN %s",
6✔
1331
                              table.name.c_str(), addColumnDefinition( copyName, defs[i] ).c_str() ) );
12✔
1332
  }
6✔
1333

1334
  // Copy the values of the moved columns over in one pass
1335
  std::string assignments;
4✔
1336
  for ( size_t i = 0; i < copyNames.size(); ++i )
10✔
1337
  {
1338
    if ( !assignments.empty() )
6✔
1339
      assignments += ", ";
2✔
1340
    assignments += sqlitePrintf( "\"%w\" = \"%w\"", copyNames[i].c_str(), defs[columnIdx + i].column.name.c_str() );
6✔
1341
  }
1342
  executeSql( sqlitePrintf( "UPDATE \"%w\" SET %s", table.name.c_str(), assignments.c_str() ) );
4✔
1343

1344
  for ( size_t i = 0; i < copyNames.size(); ++i )
10✔
1345
  {
1346
    const std::string &originalName = defs[columnIdx + i].column.name;
6✔
1347
    executeSql( sqlitePrintf( "ALTER TABLE \"%w\" DROP COLUMN \"%w\"", table.name.c_str(), originalName.c_str() ) );
6✔
1348
  }
1349
  for ( size_t i = 0; i < copyNames.size(); ++i )
10✔
1350
  {
1351
    const std::string &originalName = defs[columnIdx + i].column.name;
6✔
1352
    executeSql( sqlitePrintf( "ALTER TABLE \"%w\" RENAME COLUMN \"%w\" TO \"%w\"",
12✔
1353
                              table.name.c_str(), copyNames[i].c_str(), originalName.c_str() ) );
6✔
1354
  }
1355
}
40✔
1356

1357
void SqliteDriver::applySchemaChange( SqliteChangeApplyState &state, const ChangesetEntry &entry )
43✔
1358
{
1359
  // Clear table state to force recompute with new schema.
1360
  const std::string changedTableName = entry.schemaChangeTableName();
43✔
1361
  for ( auto it = state.tableState.begin(); it != state.tableState.end(); )
68✔
1362
  {
1363
    if ( it->first->name == changedTableName )
25✔
1364
      it = state.tableState.erase( it );
23✔
1365
    else
1366
      ++it;
2✔
1367
  }
1368

1369
  if ( const ChangesetCreateTableEntry *ctEntry = std::get_if<ChangesetCreateTableEntry>( &entry ) )
43✔
1370
  {
1371
    // TODO: Also save full CRS definition inside diff? It's pretty large and
1372
    // we'd need it for all tables with geometry columns & geometry columns
1373
    // themselves.
1374
    CrsDefinition tableCrs;
4✔
1375
    for ( const TableColumnInfo &col : ctEntry->columns )
15✔
1376
    {
1377
      if ( col.isGeometry )
11✔
1378
        tableCrs.srsId = col.geomSrsId;
2✔
1379
    }
1380

1381
    Sqlite3SavepointTransaction transaction( context(), mDb );
4✔
1382
    try
1383
    {
1384
      createTable( mDb, { ctEntry->tableName, ctEntry->columns, tableCrs } );
4✔
1385
    }
NEW
1386
    catch ( const GeoDiffException & )
×
1387
    {
1388
      // TODO: Make sure this only catches sqlite errors on CREATE TABLE
NEW
1389
      logApplyConflict( "create_table_failed", entry, true );
×
NEW
1390
      throw;
×
NEW
1391
    }
×
1392
    transaction.commitChanges();
4✔
1393
  }
4✔
1394
  else if ( const ChangesetDropTableEntry *dtEntry = std::get_if<ChangesetDropTableEntry>( &entry ) )
39✔
1395
  {
1396
    // Check there's no data in table (zero rows)
1397
    {
1398
      Sqlite3Stmt stmt;
4✔
1399
      stmt.prepare( mDb, "SELECT COUNT(*) FROM \"%w\"", dtEntry->tableName.c_str() );
4✔
1400
      if ( sqlite3_step( stmt.get() ) != SQLITE_ROW )
4✔
NEW
1401
        throwSqliteError( mDb->get(), "Getting row count in " + dtEntry->tableName );
×
1402
      if ( sqlite3_column_int( stmt.get(), 0 ) != 0 )
4✔
1403
      {
NEW
1404
        logApplyConflict( "drop_table_not_empty", entry );
×
NEW
1405
        throw GeoDiffException( "Tried to drop non-empty table " + dtEntry->tableName );
×
1406
      }
1407
    }
4✔
1408

1409
    Sqlite3Stmt stmt;
4✔
1410
    stmt.prepare( mDb, "DROP TABLE \"%w\"", dtEntry->tableName.c_str() );
4✔
1411
    if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
4✔
1412
    {
NEW
1413
      logApplyConflict( "drop_table_failed", entry, true );
×
NEW
1414
      throwSqliteError( mDb->get(), "Failure deleting table: " + dtEntry->tableName );
×
1415
    }
1416
    removeGpkgSpatialTable( mDb, dtEntry->tableName );
4✔
1417
  }
4✔
1418
  else if ( const ChangesetAddColumnEntry *acEntry = std::get_if<ChangesetAddColumnEntry>( &entry ) )
35✔
1419
  {
1420
    if ( acEntry->column.isGeometry )
22✔
1421
      // Would need changing gpkg metadata
NEW
1422
      throw GeoDiffException( "Adding geometry columns is not supported" );
×
1423
    if ( acEntry->column.isPrimaryKey )
22✔
NEW
1424
      throw GeoDiffException( "Adding column to primary key is not supported" );
×
1425
    if ( acEntry->column.isNotNull )
22✔
NEW
1426
      throw GeoDiffException( "Adding not-null column is not supported" );
×
1427

1428
    TableSchema table = tableSchema( acEntry->tableName );
22✔
1429
    if ( acEntry->columnIdx > table.columns.size() )
22✔
1430
    {
NEW
1431
      logApplyConflict( "add_column_bad_index", entry );
×
NEW
1432
      throw GeoDiffException( "Tried to add column " + acEntry->tableName + "." + acEntry->column.name +
×
NEW
1433
                              " at index " + std::to_string( acEntry->columnIdx ) + ", but the table has only " +
×
NEW
1434
                              std::to_string( table.columns.size() ) + " columns" );
×
1435
    }
1436

1437
    // Moving columns around takes several statements, so keep it atomic even if
1438
    // the caller decides to carry on after the exception.
1439
    Sqlite3SavepointTransaction transaction( context(), mDb );
22✔
1440
    try
1441
    {
1442
      addColumnAtIndex( table, acEntry->columnIdx, acEntry->column );
22✔
1443
    }
1444
    catch ( const GeoDiffException & )
1✔
1445
    {
1446
      logApplyConflict( "add_column_failed", entry );
1✔
1447
      throw;
1✔
1448
    }
1✔
1449
    transaction.commitChanges();
21✔
1450
  }
23✔
1451
  else if ( const ChangesetDropColumnEntry *dcEntry = std::get_if<ChangesetDropColumnEntry>( &entry ) )
13✔
1452
  {
1453
    if ( dcEntry->column.isGeometry )
13✔
NEW
1454
      throw GeoDiffException( "Dropping geometry columns is not supported" );
×
1455
    if ( dcEntry->column.isPrimaryKey )
13✔
NEW
1456
      throw GeoDiffException( "Dropping column from primary key is not supported" );
×
1457

1458
    TableSchema table = tableSchema( dcEntry->tableName );
13✔
1459
    if ( dcEntry->columnIdx >= table.columns.size() ||
26✔
1460
         table.columns[dcEntry->columnIdx].name != dcEntry->column.name )
13✔
1461
    {
NEW
1462
      logApplyConflict( "drop_column_index_mismatch", entry );
×
NEW
1463
      throw GeoDiffException( "Tried to drop column " + dcEntry->tableName + "." + dcEntry->column.name +
×
NEW
1464
                              " at index " + std::to_string( dcEntry->columnIdx ) + ", where the table has " +
×
NEW
1465
                              ( dcEntry->columnIdx < table.columns.size()
×
NEW
1466
                                ? table.columns[dcEntry->columnIdx].name
×
NEW
1467
                                : "no column" ) );
×
1468
    }
1469

1470
    // Check there's no data in the column (all NULLs)
1471
    {
1472
      Sqlite3Stmt stmt;
13✔
1473
      stmt.prepare( mDb, "SELECT COUNT(*) FROM \"%w\" WHERE \"%w\" IS NOT NULL",
13✔
1474
                    dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
1475
      if ( sqlite3_step( stmt.get() ) != SQLITE_ROW )
13✔
NEW
1476
        throwSqliteError( mDb->get(), "Getting row count in " + dcEntry->tableName + "." + dcEntry->column.name );
×
1477
      if ( sqlite3_column_int( stmt.get(), 0 ) != 0 )
13✔
1478
      {
NEW
1479
        logApplyConflict( "drop_column_not_empty", entry );
×
NEW
1480
        throw GeoDiffException( "Tried to drop non-empty column " + dcEntry->tableName + "." + dcEntry->column.name );
×
1481
      }
1482
    }
13✔
1483

1484
    Sqlite3Stmt stmt;
13✔
1485
    stmt.prepare( mDb, "ALTER TABLE \"%w\" DROP COLUMN \"%w\"",
13✔
1486
                  dcEntry->tableName.c_str(), dcEntry->column.name.c_str() );
1487
    if ( sqlite3_step( stmt.get() ) != SQLITE_DONE )
13✔
1488
    {
NEW
1489
      logApplyConflict( "drop_column_failed", entry, true );
×
NEW
1490
      throwSqliteError( mDb->get(), "Failure deleting column: " + dcEntry->tableName + "." + dcEntry->column.name );
×
1491
    }
1492
  }
13✔
1493
  else
1494
  {
1495
    throw GeoDiffException( "Unhandled entry type (should have been schema change) "
NEW
1496
                            + std::to_string( entry.index() ) );
×
1497
  }
1498
}
47✔
1499

1500
void SqliteDriver::applyChangeset( ChangesetReader &reader )
150✔
1501
{
1502
  TableSchema tbl;
150✔
1503

1504
  // this will acquire DB mutex and release it when the function ends (or when an exception is thrown)
1505
  Sqlite3DbMutexLocker dbMutexLocker( mDb );
150✔
1506

1507
  // start transaction!
1508
  Sqlite3SavepointTransaction savepointTransaction( context(), mDb );
150✔
1509

1510
  // Defer verifying foreign key constraints until end of transaction. This
1511
  // only applies inside our transaction, so we don't need to reset it.
1512
  Sqlite3Stmt statement;
150✔
1513
  statement.prepare( mDb, "pragma defer_foreign_keys = 1" );
150✔
1514
  int rc = sqlite3_step( statement.get() );
150✔
1515
  if ( SQLITE_DONE != rc )
150✔
NEW
1516
    logSqliteError( context(), mDb, "Failed to defer foreign key checks" );
×
1517
  statement.close();
150✔
1518

1519
  // get all triggers sql commands
1520
  // that we do not recognize (gpkg triggers are filtered)
1521
  std::vector<std::string> triggerNames;
150✔
1522
  std::vector<std::string> triggerCmds;
150✔
1523
  sqliteTriggers( context(), mDb, triggerNames, triggerCmds );
150✔
1524

1525
  for ( const std::string &name : triggerNames )
152✔
1526
  {
1527
    statement.prepare( mDb, "drop trigger '%q'", name.c_str() );
2✔
1528
    rc = sqlite3_step( statement.get() );
2✔
1529
    if ( SQLITE_DONE != rc )
2✔
1530
    {
NEW
1531
      logSqliteError( context(), mDb, "Failed to drop trigger " + name );
×
1532
    }
1533
    statement.close();
2✔
1534
  }
1535

1536
  // Applying some entries may fail due to constraints, since they require the
1537
  // entries to be in some specific, unknown order. To work around this, we
1538
  // retry applying the conflicting entries until either we apply them all or we
1539
  // get stuck.
1540
  //
1541
  // We can only reorder data entries, not schema-changing DDL entries, so we
1542
  // gather conflicting data entries in a list until either we run out of
1543
  // entries or read a schema-change entry.
1544

1545
  int unrecoverableConflictCount = 0;
150✔
1546
  std::vector<ChangesetDataEntry> conflictingEntries;
150✔
1547
  ChangesetEntry entry;
150✔
1548
  SqliteChangeApplyState state;
150✔
1549
  while ( true )
1550
  {
1551
    bool haveEntry = reader.nextEntry( entry );
630✔
1552
    if ( !haveEntry || !std::holds_alternative<ChangesetDataEntry>( entry ) )
629✔
1553
    {
1554
      // We can't reorder entries beyond this point (see above), retry applying
1555
      // conflicting ones.
1556
      std::vector<ChangesetDataEntry> newConflictingEntries;
191✔
1557
      while ( conflictingEntries.size() > 0 )
191✔
1558
      {
1559
        for ( const ChangesetDataEntry &centry : conflictingEntries )
4✔
1560
        {
1561
          ChangeApplyResult res = applyDataChange( state, centry );
2✔
1562
          switch ( res )
2✔
1563
          {
NEW
1564
            case ChangeApplyResult::Applied:
×
1565
            case ChangeApplyResult::Skipped:
NEW
1566
              break; // Applied correctly, don't put it in the new list.
×
1567
            case ChangeApplyResult::ConstraintConflict:
2✔
1568
              newConflictingEntries.push_back( centry ); // Still conflicting, keep in list.
2✔
1569
              break;
2✔
NEW
1570
            case ChangeApplyResult::NoChange:
×
NEW
1571
              unrecoverableConflictCount++; // Other issue, will throw at the end.
×
NEW
1572
              break;
×
1573
          }
1574
        }
1575

1576
        // If we haven't been able to apply any of the conflicting entries this
1577
        // loop, then these conflicts can't be resolved by reordering entries.
1578
        if ( newConflictingEntries.size() == conflictingEntries.size() )
2✔
1579
        {
1580
          for ( const ChangesetDataEntry &centry : conflictingEntries )
4✔
1581
            logApplyConflict( "unresolvable_conflict", centry );
6✔
1582
          throw GeoDiffConflictsException( "Could not resolve dependencies in constraint conflicts." );
6✔
1583
        }
NEW
1584
        conflictingEntries = newConflictingEntries;
×
NEW
1585
        newConflictingEntries.clear();
×
1586
      }
1587
    }
191✔
1588
    if ( !haveEntry )
627✔
1589
      break;
146✔
1590

1591
    if ( const ChangesetDataEntry *dataEntry = std::get_if<ChangesetDataEntry>( &entry ) )
481✔
1592
    {
1593
      ChangeApplyResult res = applyDataChange( state, *dataEntry );
438✔
1594
      switch ( res )
438✔
1595
      {
1596
        case ChangeApplyResult::Applied:
433✔
1597
        case ChangeApplyResult::Skipped:
1598
          break; // Applied correctly, continue onward.
433✔
1599
        case ChangeApplyResult::ConstraintConflict:
2✔
1600
          // Ordering conflict found, handle later.
1601
          conflictingEntries.push_back( *dataEntry );
2✔
1602
          break;
2✔
1603
        case ChangeApplyResult::NoChange:
3✔
1604
          unrecoverableConflictCount++; // Other issue, will throw at the end.
3✔
1605
          break;
3✔
1606
      }
1607
    }
1608
    else
1609
    {
1610
      applySchemaChange( state, entry );
43✔
1611
    }
1612
  }
480✔
1613

1614
  // recreate triggers
1615
  for ( const std::string &cmd : triggerCmds )
148✔
1616
  {
1617
    statement.prepare( mDb, "%s", cmd.c_str() );
2✔
1618
    if ( SQLITE_DONE != sqlite3_step( statement.get() ) )
2✔
1619
    {
NEW
1620
      logSqliteError( context(), mDb, "Failed to recreate trigger using SQL \"" + cmd + "\"" );
×
1621
    }
1622
    statement.close();
2✔
1623
  }
1624

1625
  if ( !unrecoverableConflictCount )
146✔
1626
  {
1627
    savepointTransaction.commitChanges();
143✔
1628
  }
1629
  else
1630
  {
1631
    throw GeoDiffConflictsException( "Conflicts encountered while applying changes! Total " + std::to_string( unrecoverableConflictCount ) );
3✔
1632
  }
1633
}
222✔
1634

1635
void SqliteDriver::createTables( const std::vector<TableSchema> &tables )
124✔
1636
{
1637
  // currently we always create geopackage meta tables. Maybe in the future we can skip
1638
  // that if there is a reason, and have that optional if none of the tables are spatial.
1639

1640
  Sqlite3Stmt stmt1;
124✔
1641
  stmt1.prepare( mDb, "SELECT InitSpatialMetadata('main');" );
124✔
1642
  int res = sqlite3_step( stmt1.get() );
124✔
1643
  if ( res != SQLITE_ROW )
124✔
NEW
1644
    throwSqliteError( mDb->get(), "Failure initializing spatial metadata" );
×
1645

1646
  for ( const TableSchema &tbl : tables )
257✔
1647
  {
1648
    if ( startsWith( tbl.name, "gpkg_" ) )
266✔
NEW
1649
      continue;
×
1650
    createTable( mDb, tbl );
133✔
1651
  }
1652
}
124✔
1653

1654
void SqliteDriver::dumpTableData( ChangesetWriter &writer, TableSchema tbl, bool useModified )
35✔
1655
{
1656
  std::string dbName = databaseName( useModified );
35✔
1657
  if ( !tbl.hasPrimaryKey() )
35✔
1658
    return;  // ignore tables without primary key - they can't be compared properly
1✔
1659

1660
  bool first = true;
34✔
1661
  Sqlite3Stmt statementI;
34✔
1662
  statementI.prepare( mDb, "SELECT * FROM \"%w\".\"%w\"", dbName.c_str(), tbl.name.c_str() );
34✔
1663
  int rc;
1664
  while ( SQLITE_ROW == ( rc = sqlite3_step( statementI.get() ) ) )
105✔
1665
  {
1666
    if ( first )
71✔
1667
    {
1668
      writer.beginTable( schemaToChangesetTable( tbl.name, tbl ) );
33✔
1669
      first = false;
33✔
1670
    }
1671

1672
    ChangesetDataEntry e;
71✔
1673
    e.op = ChangesetDataEntry::OpInsert;
71✔
1674
    size_t numColumns = tbl.columns.size();
71✔
1675
    for ( size_t i = 0; i < numColumns; ++i )
338✔
1676
    {
1677
      Sqlite3Value v( sqlite3_column_value( statementI.get(), static_cast<int>( i ) ) );
267✔
1678
      e.newValues.push_back( changesetValue( v.value() ) );
267✔
1679
    }
267✔
1680
    writer.writeEntry( e );
71✔
1681
  }
71✔
1682
  if ( rc != SQLITE_DONE )
34✔
1683
  {
NEW
1684
    logSqliteError( context(), mDb, "Failure dumping changeset" );
×
1685
  }
1686
}
35✔
1687

1688
void SqliteDriver::dumpData( ChangesetWriter &writer, bool useModified )
14✔
1689
{
1690
  std::vector<std::string> tables = listTables();
14✔
1691
  for ( const std::string &tableName : tables )
45✔
1692
  {
1693
    TableSchema tbl = tableSchema( tableName, useModified );
31✔
1694
    dumpTableData( writer, tbl, useModified );
31✔
1695
  }
31✔
1696
}
14✔
1697

1698
std::vector<std::vector<std::string>> SqliteDriver::executeSql( std::string sql )
326✔
1699
{
1700
  Sqlite3Stmt stmt;
326✔
1701
  stmt.prepare( mDb, "%s", sql.c_str() );
326✔
1702
  std::vector<std::vector<std::string>> rows;
326✔
1703
  int rc;
1704
  while ( ( rc = sqlite3_step( stmt.get() ) ) == SQLITE_ROW )
328✔
1705
  {
1706
    std::vector<std::string> values;
2✔
1707
    values.resize( sqlite3_column_count( stmt.get() ) );
2✔
1708
    for ( size_t i = 0; i < values.size(); ++i )
6✔
1709
    {
1710
      const unsigned char *text = sqlite3_column_text( stmt.get(), static_cast<int>( i ) );
4✔
1711
      if ( text )
4✔
1712
        values[i] = reinterpret_cast<const char *>( text );
4✔
1713
      else
NEW
1714
        values[i] = "<NULL>";
×
1715
    }
1716
    rows.push_back( values );
2✔
1717
  }
2✔
1718
  if ( rc != SQLITE_DONE )
326✔
1719
  {
NEW
1720
    throwSqliteError( mDb->get(), "Failure executing SQL: " + sql );
×
1721
  }
1722
  return rows;
652✔
1723
}
326✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc