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
45class GpkgError : public std::runtime_error {
46public:
54 explicit GpkgError(const std::string& msg) : std::runtime_error(msg) {}
61 GpkgError(const std::string& msg, int rc)
62 : std::runtime_error(msg + " (sqlite rc=" + std::to_string(rc) + ")") {}
63};
64
65// ============================================================================
66// RAII: Database handle
67// ============================================================================
68
72struct DbDeleter {
82 void operator()(sqlite3* db) const noexcept {
83 if (db)
84 sqlite3_close_v2(db);
85 }
86};
87using DbPtr = std::unique_ptr<sqlite3, DbDeleter>;
88
103inline DbPtr open_database(const std::string& path,
104 int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) {
105 sqlite3* raw = nullptr;
106 int rc = sqlite3_open_v2(path.c_str(), &raw, flags, nullptr);
107 DbPtr db(raw);
108 if (rc != SQLITE_OK) {
109 std::string msg = raw ? sqlite3_errmsg(raw) : "unknown error";
110 throw GpkgError("Failed to open database '" + path + "': " + msg, rc);
111 }
112
113 // Slice IO-5: every connection enforces foreign keys. This is a
114 // per-connection SQLite setting, so we set it here rather than only
115 // in create_schema — reader connections need it too if they want
116 // cascading-delete or orphan-rejection semantics to hold.
117 char* err = nullptr;
118 if (sqlite3_exec(db.get(), "PRAGMA foreign_keys=ON", nullptr, nullptr, &err) != SQLITE_OK) {
119 std::string msg = err ? err : "unknown error";
120 sqlite3_free(err);
121 throw GpkgError("Failed to enable foreign_keys pragma on '" + path + "': " + msg, rc);
122 }
123
124 // Retry on a busy database rather than failing immediately with
125 // SQLITE_BUSY. On Windows (mandatory file locking) a connection that is
126 // torn down while a write is still settling can briefly hold the .gpkg /
127 // -wal lock; without a busy handler the next open()'s first PRAGMA fails
128 // outright ("database is locked"). 5 s is generous for the short-lived
129 // intra-process contention we actually hit and is a no-op on POSIX, where
130 // advisory locking already tolerates this.
131 sqlite3_busy_timeout(db.get(), 5000);
132
133 return db;
134}
135
136// ============================================================================
137// RAII: Prepared statement
138// ============================================================================
139
151 void operator()(sqlite3_stmt* stmt) const noexcept {
152 if (stmt)
153 sqlite3_finalize(stmt);
154 }
155};
156using StmtPtr = std::unique_ptr<sqlite3_stmt, StmtDeleter>;
157
171inline StmtPtr prepare(sqlite3* db, const std::string& sql) {
172 sqlite3_stmt* raw = nullptr;
173 int rc = sqlite3_prepare_v2(db, sql.c_str(), static_cast<int>(sql.size()), &raw, nullptr);
174 StmtPtr stmt(raw);
175 if (rc != SQLITE_OK) {
176 throw GpkgError("Failed to prepare: " + sql + " — " + sqlite3_errmsg(db), rc);
177 }
178 return stmt;
179}
180
181// ============================================================================
182// Helpers
183// ============================================================================
184
198inline void exec(sqlite3* db, const std::string& sql) {
199 char* errmsg = nullptr;
200 int rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg);
201 if (rc != SQLITE_OK) {
202 std::string msg = errmsg ? errmsg : "unknown error";
203 sqlite3_free(errmsg);
204 throw GpkgError("exec failed: " + msg + " — SQL: " + sql, rc);
205 }
206}
207
220inline void bind_text(sqlite3_stmt* stmt, int col, const std::string& val) {
221 sqlite3_bind_text(stmt, col, val.c_str(), static_cast<int>(val.size()), SQLITE_TRANSIENT);
222}
223
233inline void bind_double(sqlite3_stmt* stmt, int col, double val) {
234 sqlite3_bind_double(stmt, col, val);
235}
236
246inline void bind_int(sqlite3_stmt* stmt, int col, int val) {
247 sqlite3_bind_int(stmt, col, val);
248}
249
258inline void bind_null(sqlite3_stmt* stmt, int col) {
259 sqlite3_bind_null(stmt, col);
260}
261
276inline void bind_blob(sqlite3_stmt* stmt, int col, const void* data, int size) {
277 sqlite3_bind_blob(stmt, col, data, size, SQLITE_TRANSIENT);
278}
279
292inline std::string column_text(sqlite3_stmt* stmt, int col) {
293 const unsigned char* txt = sqlite3_column_text(stmt, col);
294 return txt ? std::string(reinterpret_cast<const char*>(txt)) : std::string{};
295}
296
306inline double column_double(sqlite3_stmt* stmt, int col) {
307 return sqlite3_column_double(stmt, col);
308}
309
318inline int column_int(sqlite3_stmt* stmt, int col) {
319 return sqlite3_column_int(stmt, col);
320}
321
330inline bool column_is_null(sqlite3_stmt* stmt, int col) {
331 return sqlite3_column_type(stmt, col) == SQLITE_NULL;
332}
333
346inline std::vector<uint8_t> column_blob(sqlite3_stmt* stmt, int col) {
347 int size = sqlite3_column_bytes(stmt, col);
348 const uint8_t* data = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, col));
349 if (!data || size <= 0)
350 return {};
351 return {data, data + size};
352}
353
354// RAII transaction guard.
355//
356// The destructor rolls a not-yet-committed transaction back, but does so
357// ROBUSTLY instead of fire-and-forget. A plain, unchecked
358// `sqlite3_exec(db, "ROLLBACK")` can silently leave the transaction OPEN on
359// Windows: if any statement on the connection is still in an aborted/stepped
360// state — e.g. an INSERT that just tripped an immediate FOREIGN KEY constraint
361// mid-write — SQLite refuses the ROLLBACK with SQLITE_BUSY ("cannot rollback -
362// SQL statements in progress"). Under Windows' mandatory file locking the
363// transaction then stays open and the SAME connection reads its own uncommitted
364// rows; POSIX advisory locking tolerates the stray lock, which is why the bug
365// only surfaced on Windows (test_engine_geopackage_mesh2d rollback case, where
366// the connection saw 3 nodes / 4 vertices / 60 options instead of none).
367//
368// Resetting only the one statement that failed (in step_or_throw) was not
369// enough — anything still holding a per-statement lock at unwind time blocks the
370// ROLLBACK. rollback() therefore drives the connection to a known-clean state:
371// 1. reset EVERY live statement so none holds a statement-journal lock,
372// 2. issue ROLLBACK,
373// 3. confirm the connection actually returned to autocommit mode,
374// retrying a bounded number of times. On POSIX this is a single clean pass and
375// the reset loop is a no-op (unwinding has already finalized the statements).
384public:
395 explicit Transaction(sqlite3* db) : db_(db) { exec(db_, "BEGIN IMMEDIATE"); }
402 void commit() {
403 if (!finished_) {
404 exec(db_, "COMMIT");
405 finished_ = true;
406 }
407 }
408
416 void rollback() {
417 if (finished_)
418 return;
419 for (int attempt = 0; attempt < 3; ++attempt) {
420 // Clear any statement left mid-flight so it cannot hold a lock that
421 // makes ROLLBACK fail with SQLITE_BUSY (statements in progress).
422 for (sqlite3_stmt* s = sqlite3_next_stmt(db_, nullptr); s != nullptr;
423 s = sqlite3_next_stmt(db_, s)) {
424 sqlite3_reset(s);
425 }
426 sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr);
427 // sqlite3_get_autocommit() != 0 iff no transaction is open, i.e. the
428 // ROLLBACK actually took effect. If it is still 0 the rollback was
429 // refused; loop and retry now that the statements are reset.
430 if (sqlite3_get_autocommit(db_) != 0)
431 break;
432 }
433 finished_ = true;
434 }
435
445 Transaction(const Transaction&) = delete;
450
451private:
452 sqlite3* db_;
453 bool finished_ = false;
454};
455
456} // namespace openswmm::gpkg
457
458#endif // OPENSWMM_GPKG_UTILS_HPP
Runtime-error carrier for owned GeoPackage/SQLite diagnostic text.
Definition GpkgUtils.hpp:45
GpkgError(const std::string &msg, int rc)
Own a failure message with a formatted SQLite result code.
Definition GpkgUtils.hpp:61
GpkgError(const std::string &msg)
Own a supplied GeoPackage failure message.
Definition GpkgUtils.hpp:54
Transaction(sqlite3 *db)
Borrow a connection and begin an immediate write transaction.
Definition GpkgUtils.hpp:395
void commit()
Commit an unfinished transaction and mark the guard finished.
Definition GpkgUtils.hpp:402
~Transaction()
Attempt best-effort rollback unless already committed/rolled back.
Definition GpkgUtils.hpp:441
void rollback()
Best-effort reset all live statements and roll back an unfinished transaction.
Definition GpkgUtils.hpp:416
Transaction & operator=(const Transaction &)=delete
Disallow copy replacement of a transaction guard.
Transaction(const Transaction &)=delete
Disallow copying one transaction cleanup responsibility.
Definition ExternalContentReader.cpp:50
std::unique_ptr< sqlite3_stmt, StmtDeleter > StmtPtr
Definition GpkgUtils.hpp:156
void exec(sqlite3 *db, const std::string &sql)
Execute SQL text and discard all returned rows.
Definition GpkgUtils.hpp:198
void bind_blob(sqlite3_stmt *stmt, int col, const void *data, int size)
Bind copied binary bytes, discarding SQLite status.
Definition GpkgUtils.hpp:276
DbPtr open_database(const std::string &path, int flags=SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE)
Open an owned connection and configure GeoPackage connection defaults.
Definition GpkgUtils.hpp:103
void bind_int(sqlite3_stmt *stmt, int col, int val)
Bind an int SQL parameter, discarding SQLite status.
Definition GpkgUtils.hpp:246
void bind_double(sqlite3_stmt *stmt, int col, double val)
Bind a floating SQL parameter, discarding SQLite status.
Definition GpkgUtils.hpp:233
StmtPtr prepare(sqlite3 *db, const std::string &sql)
Prepare the first SQL statement without stepping it.
Definition GpkgUtils.hpp:171
std::unique_ptr< sqlite3, DbDeleter > DbPtr
Definition GpkgUtils.hpp:87
void bind_null(sqlite3_stmt *stmt, int col)
Bind SQL NULL, discarding SQLite status.
Definition GpkgUtils.hpp:258
std::string column_text(sqlite3_stmt *stmt, int col)
Copy current-row text using NUL-terminated SQLite conversion.
Definition GpkgUtils.hpp:292
int column_int(sqlite3_stmt *stmt, int col)
Read current-row SQLite numeric conversion as int.
Definition GpkgUtils.hpp:318
bool column_is_null(sqlite3_stmt *stmt, int col)
Test the current-row SQLite column type for NULL.
Definition GpkgUtils.hpp:330
void bind_text(sqlite3_stmt *stmt, int col, const std::string &val)
Bind a copied SQL text parameter, discarding SQLite status.
Definition GpkgUtils.hpp:220
double column_double(sqlite3_stmt *stmt, int col)
Read current-row SQLite numeric conversion as double.
Definition GpkgUtils.hpp:306
std::vector< uint8_t > column_blob(sqlite3_stmt *stmt, int col)
Copy current-row blob bytes into owned storage.
Definition GpkgUtils.hpp:346
Unique-pointer deleter using SQLite deferred close semantics.
Definition GpkgUtils.hpp:72
void operator()(sqlite3 *db) const noexcept
Best-effort close a uniquely owned SQLite connection.
Definition GpkgUtils.hpp:82
Unique-pointer deleter that finalizes prepared statements.
Definition GpkgUtils.hpp:143
void operator()(sqlite3_stmt *stmt) const noexcept
Finalize a uniquely owned SQLite statement.
Definition GpkgUtils.hpp:151