OpenSWMM Engine  6.0.0-alpha.3
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.3)
Loading...
Searching...
No Matches
GpkgUtils.hpp
Go to the documentation of this file.
1
10
11#ifndef OPENSWMM_GPKG_UTILS_HPP
12#define OPENSWMM_GPKG_UTILS_HPP
13
14#include <sqlite3.h>
15#include <string>
16#include <stdexcept>
17#include <memory>
18#include <vector>
19
20namespace openswmm::gpkg {
21
22// ============================================================================
23// Exception
24// ============================================================================
25
26class GpkgError : public std::runtime_error {
27public:
28 explicit GpkgError(const std::string& msg) : std::runtime_error(msg) {}
29 GpkgError(const std::string& msg, int rc)
30 : std::runtime_error(msg + " (sqlite rc=" + std::to_string(rc) + ")") {}
31};
32
33// ============================================================================
34// RAII: Database handle
35// ============================================================================
36
37struct DbDeleter {
38 void operator()(sqlite3* db) const noexcept {
39 if (db) sqlite3_close_v2(db);
40 }
41};
42using DbPtr = std::unique_ptr<sqlite3, DbDeleter>;
43
44inline DbPtr open_database(const std::string& path, int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) {
45 sqlite3* raw = nullptr;
46 int rc = sqlite3_open_v2(path.c_str(), &raw, flags, nullptr);
47 DbPtr db(raw);
48 if (rc != SQLITE_OK) {
49 std::string msg = raw ? sqlite3_errmsg(raw) : "unknown error";
50 throw GpkgError("Failed to open database '" + path + "': " + msg, rc);
51 }
52
53 // Slice IO-5: every connection enforces foreign keys. This is a
54 // per-connection SQLite setting, so we set it here rather than only
55 // in create_schema — reader connections need it too if they want
56 // cascading-delete or orphan-rejection semantics to hold.
57 char* err = nullptr;
58 if (sqlite3_exec(db.get(), "PRAGMA foreign_keys=ON", nullptr, nullptr,
59 &err) != SQLITE_OK) {
60 std::string msg = err ? err : "unknown error";
61 sqlite3_free(err);
62 throw GpkgError("Failed to enable foreign_keys pragma on '" + path
63 + "': " + msg, rc);
64 }
65
66 // Retry on a busy database rather than failing immediately with
67 // SQLITE_BUSY. On Windows (mandatory file locking) a connection that is
68 // torn down while a write is still settling can briefly hold the .gpkg /
69 // -wal lock; without a busy handler the next open()'s first PRAGMA fails
70 // outright ("database is locked"). 5 s is generous for the short-lived
71 // intra-process contention we actually hit and is a no-op on POSIX, where
72 // advisory locking already tolerates this.
73 sqlite3_busy_timeout(db.get(), 5000);
74
75 return db;
76}
77
78// ============================================================================
79// RAII: Prepared statement
80// ============================================================================
81
83 void operator()(sqlite3_stmt* stmt) const noexcept {
84 if (stmt) sqlite3_finalize(stmt);
85 }
86};
87using StmtPtr = std::unique_ptr<sqlite3_stmt, StmtDeleter>;
88
89inline StmtPtr prepare(sqlite3* db, const std::string& sql) {
90 sqlite3_stmt* raw = nullptr;
91 int rc = sqlite3_prepare_v2(db, sql.c_str(), static_cast<int>(sql.size()), &raw, nullptr);
92 StmtPtr stmt(raw);
93 if (rc != SQLITE_OK) {
94 throw GpkgError("Failed to prepare: " + sql + " — " + sqlite3_errmsg(db), rc);
95 }
96 return stmt;
97}
98
99// ============================================================================
100// Helpers
101// ============================================================================
102
103inline void exec(sqlite3* db, const std::string& sql) {
104 char* errmsg = nullptr;
105 int rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg);
106 if (rc != SQLITE_OK) {
107 std::string msg = errmsg ? errmsg : "unknown error";
108 sqlite3_free(errmsg);
109 throw GpkgError("exec failed: " + msg + " — SQL: " + sql, rc);
110 }
111}
112
113inline void bind_text(sqlite3_stmt* stmt, int col, const std::string& val) {
114 sqlite3_bind_text(stmt, col, val.c_str(), static_cast<int>(val.size()), SQLITE_TRANSIENT);
115}
116
117inline void bind_double(sqlite3_stmt* stmt, int col, double val) {
118 sqlite3_bind_double(stmt, col, val);
119}
120
121inline void bind_int(sqlite3_stmt* stmt, int col, int val) {
122 sqlite3_bind_int(stmt, col, val);
123}
124
125inline void bind_null(sqlite3_stmt* stmt, int col) {
126 sqlite3_bind_null(stmt, col);
127}
128
129inline void bind_blob(sqlite3_stmt* stmt, int col, const void* data, int size) {
130 sqlite3_bind_blob(stmt, col, data, size, SQLITE_TRANSIENT);
131}
132
133inline std::string column_text(sqlite3_stmt* stmt, int col) {
134 const unsigned char* txt = sqlite3_column_text(stmt, col);
135 return txt ? std::string(reinterpret_cast<const char*>(txt)) : std::string{};
136}
137
138inline double column_double(sqlite3_stmt* stmt, int col) {
139 return sqlite3_column_double(stmt, col);
140}
141
142inline int column_int(sqlite3_stmt* stmt, int col) {
143 return sqlite3_column_int(stmt, col);
144}
145
146inline bool column_is_null(sqlite3_stmt* stmt, int col) {
147 return sqlite3_column_type(stmt, col) == SQLITE_NULL;
148}
149
150inline std::vector<uint8_t> column_blob(sqlite3_stmt* stmt, int col) {
151 int size = sqlite3_column_bytes(stmt, col);
152 const uint8_t* data = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, col));
153 if (!data || size <= 0) return {};
154 return {data, data + size};
155}
156
157// RAII transaction guard.
158//
159// The destructor rolls a not-yet-committed transaction back, but does so
160// ROBUSTLY instead of fire-and-forget. A plain, unchecked
161// `sqlite3_exec(db, "ROLLBACK")` can silently leave the transaction OPEN on
162// Windows: if any statement on the connection is still in an aborted/stepped
163// state — e.g. an INSERT that just tripped an immediate FOREIGN KEY constraint
164// mid-write — SQLite refuses the ROLLBACK with SQLITE_BUSY ("cannot rollback -
165// SQL statements in progress"). Under Windows' mandatory file locking the
166// transaction then stays open and the SAME connection reads its own uncommitted
167// rows; POSIX advisory locking tolerates the stray lock, which is why the bug
168// only surfaced on Windows (test_engine_geopackage_mesh2d rollback case, where
169// the connection saw 3 nodes / 4 vertices / 60 options instead of none).
170//
171// Resetting only the one statement that failed (in step_or_throw) was not
172// enough — anything still holding a per-statement lock at unwind time blocks the
173// ROLLBACK. rollback() therefore drives the connection to a known-clean state:
174// 1. reset EVERY live statement so none holds a statement-journal lock,
175// 2. issue ROLLBACK,
176// 3. confirm the connection actually returned to autocommit mode,
177// retrying a bounded number of times. On POSIX this is a single clean pass and
178// the reset loop is a no-op (unwinding has already finalized the statements).
180public:
181 explicit Transaction(sqlite3* db) : db_(db) {
182 exec(db_, "BEGIN IMMEDIATE");
183 }
184 void commit() {
185 if (!finished_) {
186 exec(db_, "COMMIT");
187 finished_ = true;
188 }
189 }
190 void rollback() {
191 if (finished_) return;
192 for (int attempt = 0; attempt < 3; ++attempt) {
193 // Clear any statement left mid-flight so it cannot hold a lock that
194 // makes ROLLBACK fail with SQLITE_BUSY (statements in progress).
195 for (sqlite3_stmt* s = sqlite3_next_stmt(db_, nullptr); s != nullptr;
196 s = sqlite3_next_stmt(db_, s)) {
197 sqlite3_reset(s);
198 }
199 sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr);
200 // sqlite3_get_autocommit() != 0 iff no transaction is open, i.e. the
201 // ROLLBACK actually took effect. If it is still 0 the rollback was
202 // refused; loop and retry now that the statements are reset.
203 if (sqlite3_get_autocommit(db_) != 0) break;
204 }
205 finished_ = true;
206 }
208 rollback();
209 }
210 Transaction(const Transaction&) = delete;
212private:
213 sqlite3* db_;
214 bool finished_ = false;
215};
216
217} // namespace openswmm::gpkg
218
219#endif // OPENSWMM_GPKG_UTILS_HPP
Definition GpkgUtils.hpp:26
GpkgError(const std::string &msg, int rc)
Definition GpkgUtils.hpp:29
GpkgError(const std::string &msg)
Definition GpkgUtils.hpp:28
Transaction(sqlite3 *db)
Definition GpkgUtils.hpp:181
void commit()
Definition GpkgUtils.hpp:184
~Transaction()
Definition GpkgUtils.hpp:207
void rollback()
Definition GpkgUtils.hpp:190
Transaction & operator=(const Transaction &)=delete
Transaction(const Transaction &)=delete
Definition ExternalContentReader.cpp:33
std::unique_ptr< sqlite3_stmt, StmtDeleter > StmtPtr
Definition GpkgUtils.hpp:87
void exec(sqlite3 *db, const std::string &sql)
Definition GpkgUtils.hpp:103
void bind_blob(sqlite3_stmt *stmt, int col, const void *data, int size)
Definition GpkgUtils.hpp:129
DbPtr open_database(const std::string &path, int flags=SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE)
Definition GpkgUtils.hpp:44
void bind_int(sqlite3_stmt *stmt, int col, int val)
Definition GpkgUtils.hpp:121
void bind_double(sqlite3_stmt *stmt, int col, double val)
Definition GpkgUtils.hpp:117
StmtPtr prepare(sqlite3 *db, const std::string &sql)
Definition GpkgUtils.hpp:89
std::unique_ptr< sqlite3, DbDeleter > DbPtr
Definition GpkgUtils.hpp:42
void bind_null(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:125
std::string column_text(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:133
int column_int(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:142
bool column_is_null(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:146
void bind_text(sqlite3_stmt *stmt, int col, const std::string &val)
Definition GpkgUtils.hpp:113
double column_double(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:138
std::vector< uint8_t > column_blob(sqlite3_stmt *stmt, int col)
Definition GpkgUtils.hpp:150
Definition GpkgUtils.hpp:37
void operator()(sqlite3 *db) const noexcept
Definition GpkgUtils.hpp:38
Definition GpkgUtils.hpp:82
void operator()(sqlite3_stmt *stmt) const noexcept
Definition GpkgUtils.hpp:83