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