#include "main.h" #include #include "brush_database.h" BrushDatabase g_brush_database; namespace { constexpr int kBrushDatabaseSchemaVersion = 6; constexpr const char* kBrushSelectColumns = "id, name, type, look_id, z_order, source_file, server_look_id, " "draggable, on_blocking, on_duplicate, redo_borders, randomize, " "one_size, solo_optional, thickness, thickness_ceiling"; constexpr const char* kRecreateTilesetTablesSql = "DROP TABLE IF EXISTS tileset_brush_entries;" "DROP TABLE IF EXISTS tileset_sections;" "DROP TABLE IF EXISTS tilesets;" "CREATE TABLE tilesets (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL UNIQUE," "source_file TEXT NOT NULL DEFAULT ''," "created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," "updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE tileset_sections (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "tileset_id INTEGER NOT NULL," "section_type TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (tileset_id) REFERENCES tilesets(id) ON DELETE CASCADE" ");" "CREATE TABLE tileset_brush_entries (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "tileset_section_id INTEGER NOT NULL," "entry_kind TEXT NOT NULL DEFAULT 'brush'," "brush_id INTEGER," "brush_name TEXT NOT NULL DEFAULT ''," "item_id INTEGER NOT NULL DEFAULT 0," "from_item_id INTEGER NOT NULL DEFAULT 0," "to_item_id INTEGER NOT NULL DEFAULT 0," "after_brush_name TEXT NOT NULL DEFAULT ''," "after_item_id INTEGER NOT NULL DEFAULT 0," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (tileset_section_id) REFERENCES tileset_sections(id) ON DELETE CASCADE," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE SET NULL" ");" "CREATE INDEX idx_tileset_brush_entries_section ON tileset_brush_entries(tileset_section_id, sort_order);" "CREATE INDEX idx_tileset_brush_entries_name ON tileset_brush_entries(brush_name);" "CREATE INDEX idx_tileset_brush_entries_item ON tileset_brush_entries(item_id, from_item_id, to_item_id);"; wxString ToWxString(const char* value) { return value ? wxString::FromUTF8(value) : wxString(); } wxString MakeTransactionSavepointName(int savepointId) { return wxString::Format("brushdb_sp_%d", savepointId); } void FinalizeStatements(std::initializer_list statements) { for (sqlite3_stmt* stmt : statements) { if (stmt) { sqlite3_finalize(stmt); } } } void BindNullableInt64(sqlite3_stmt* stmt, int index, int64_t value) { if (value > 0) { sqlite3_bind_int64(stmt, index, value); } else { sqlite3_bind_null(stmt, index); } } int64_t ReadNullableInt64(sqlite3_stmt* stmt, int index) { return sqlite3_column_type(stmt, index) == SQLITE_NULL ? 0 : sqlite3_column_int64(stmt, index); } int BindBrushRecordFields(sqlite3_stmt* stmt, const BrushRecord &brush, int parameterIndex) { sqlite3_bind_text(stmt, parameterIndex++, brush.name.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, parameterIndex++, brush.type.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, parameterIndex++, brush.lookId); sqlite3_bind_int(stmt, parameterIndex++, brush.zOrder); sqlite3_bind_text(stmt, parameterIndex++, brush.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, parameterIndex++, brush.serverLookId); sqlite3_bind_int(stmt, parameterIndex++, brush.draggable ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.onBlocking ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.onDuplicate ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.redoBorders ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.randomize ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.oneSize ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.soloOptional ? 1 : 0); sqlite3_bind_int(stmt, parameterIndex++, brush.thickness); sqlite3_bind_int(stmt, parameterIndex++, brush.thicknessCeiling); return parameterIndex; } struct AlignedNodeWriteContext { sqlite3_stmt* insertNodeStmt = nullptr; sqlite3_stmt* insertItemStmt = nullptr; wxString insertNodeError; wxString insertItemError; }; void ReadBrushRecordFromStatement(sqlite3_stmt* stmt, BrushRecord &outBrush) { outBrush.id = sqlite3_column_int64(stmt, 0); outBrush.name = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 1))); outBrush.type = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 2))); outBrush.lookId = sqlite3_column_int(stmt, 3); outBrush.zOrder = sqlite3_column_int(stmt, 4); outBrush.sourceFile = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 5))); outBrush.serverLookId = sqlite3_column_int(stmt, 6); outBrush.draggable = sqlite3_column_int(stmt, 7) != 0; outBrush.onBlocking = sqlite3_column_int(stmt, 8) != 0; outBrush.onDuplicate = sqlite3_column_int(stmt, 9) != 0; outBrush.redoBorders = sqlite3_column_int(stmt, 10) != 0; outBrush.randomize = sqlite3_column_int(stmt, 11) != 0; outBrush.oneSize = sqlite3_column_int(stmt, 12) != 0; outBrush.soloOptional = sqlite3_column_int(stmt, 13) != 0; outBrush.thickness = sqlite3_column_int(stmt, 14); outBrush.thicknessCeiling = sqlite3_column_int(stmt, 15); } template bool WriteAlignedNodesWithItems( sqlite3* connection, int64_t brushId, const std::vector &nodes, const AlignedNodeWriteContext &context, SetErrorFn &&setErrorFromDatabase ) { for (const NodeRecord &node : nodes) { sqlite3_reset(context.insertNodeStmt); sqlite3_clear_bindings(context.insertNodeStmt); sqlite3_bind_int64(context.insertNodeStmt, 1, brushId); sqlite3_bind_text(context.insertNodeStmt, 2, node.align.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(context.insertNodeStmt, 3, node.sortOrder); int rc = sqlite3_step(context.insertNodeStmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase(context.insertNodeError); } const int64_t nodeId = sqlite3_last_insert_rowid(connection); for (const ItemRecord &item : node.items) { sqlite3_reset(context.insertItemStmt); sqlite3_clear_bindings(context.insertItemStmt); sqlite3_bind_int64(context.insertItemStmt, 1, nodeId); sqlite3_bind_int(context.insertItemStmt, 2, item.itemId); sqlite3_bind_int(context.insertItemStmt, 3, item.chance); sqlite3_bind_int(context.insertItemStmt, 4, item.sortOrder); rc = sqlite3_step(context.insertItemStmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase(context.insertItemError); } } } return true; } template bool ReadAlignedNodesWithItems( sqlite3_stmt* nodeStmt, sqlite3_stmt* itemStmt, std::vector &outNodes, const wxString &readNodesError, const wxString &readItemsError, SetErrorFn &&setErrorFromDatabase ) { for (;;) { const int nodeRc = sqlite3_step(nodeStmt); if (nodeRc == SQLITE_DONE) { break; } if (nodeRc != SQLITE_ROW) { return setErrorFromDatabase(readNodesError); } const int64_t nodeId = sqlite3_column_int64(nodeStmt, 0); NodeRecord node; node.align = ToWxString(reinterpret_cast(sqlite3_column_text(nodeStmt, 1))); node.sortOrder = sqlite3_column_int(nodeStmt, 2); sqlite3_reset(itemStmt); sqlite3_clear_bindings(itemStmt); sqlite3_bind_int64(itemStmt, 1, nodeId); for (;;) { const int itemRc = sqlite3_step(itemStmt); if (itemRc == SQLITE_DONE) { break; } if (itemRc != SQLITE_ROW) { return setErrorFromDatabase(readItemsError); } ItemRecord item; item.itemId = sqlite3_column_int(itemStmt, 0); item.chance = sqlite3_column_int(itemStmt, 1); item.sortOrder = sqlite3_column_int(itemStmt, 2); node.items.push_back(item); } outNodes.push_back(node); } return true; } template bool WriteGroundBorderConditions( int64_t caseId, const std::vector &conditions, sqlite3_stmt* insertConditionStmt, SetErrorFn &&setErrorFromDatabase ) { for (const GroundBorderCaseConditionRecord &condition : conditions) { sqlite3_reset(insertConditionStmt); sqlite3_clear_bindings(insertConditionStmt); sqlite3_bind_int64(insertConditionStmt, 1, caseId); sqlite3_bind_text(insertConditionStmt, 2, condition.conditionType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertConditionStmt, 3, condition.matchValue); sqlite3_bind_text(insertConditionStmt, 4, condition.edge.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertConditionStmt, 5, condition.sortOrder); if (sqlite3_step(insertConditionStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert ground border condition"); } } return true; } template bool WriteGroundBorderActions( int64_t caseId, const std::vector &actions, sqlite3_stmt* insertActionStmt, SetErrorFn &&setErrorFromDatabase ) { for (const GroundBorderCaseActionRecord &action : actions) { sqlite3_reset(insertActionStmt); sqlite3_clear_bindings(insertActionStmt); sqlite3_bind_int64(insertActionStmt, 1, caseId); sqlite3_bind_text(insertActionStmt, 2, action.actionType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertActionStmt, 3, action.targetValue); sqlite3_bind_text(insertActionStmt, 4, action.edge.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertActionStmt, 5, action.replacementValue); sqlite3_bind_int(insertActionStmt, 6, action.sortOrder); if (sqlite3_step(insertActionStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert ground border action"); } } return true; } template bool WriteGroundBorderCases( sqlite3* connection, int64_t groundBrushBorderId, const std::vector &cases, sqlite3_stmt* insertCaseStmt, sqlite3_stmt* insertConditionStmt, sqlite3_stmt* insertActionStmt, SetErrorFn &&setErrorFromDatabase ) { for (const GroundBorderCaseRecord &caseRecord : cases) { sqlite3_reset(insertCaseStmt); sqlite3_clear_bindings(insertCaseStmt); sqlite3_bind_int64(insertCaseStmt, 1, groundBrushBorderId); sqlite3_bind_int(insertCaseStmt, 2, caseRecord.sortOrder); if (sqlite3_step(insertCaseStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert ground border case"); } const int64_t caseId = sqlite3_last_insert_rowid(connection); if (!WriteGroundBorderConditions(caseId, caseRecord.conditions, insertConditionStmt, setErrorFromDatabase)) { return false; } if (!WriteGroundBorderActions(caseId, caseRecord.actions, insertActionStmt, setErrorFromDatabase)) { return false; } } return true; } template bool ReadGroundBorderConditions( int64_t caseId, sqlite3_stmt* conditionStmt, std::vector &outConditions, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(conditionStmt); sqlite3_clear_bindings(conditionStmt); sqlite3_bind_int64(conditionStmt, 1, caseId); for (;;) { const int conditionRc = sqlite3_step(conditionStmt); if (conditionRc == SQLITE_DONE) { break; } if (conditionRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read ground border case conditions"); } GroundBorderCaseConditionRecord condition; condition.conditionType = ToWxString(reinterpret_cast(sqlite3_column_text(conditionStmt, 0))); condition.matchValue = sqlite3_column_int(conditionStmt, 1); condition.edge = ToWxString(reinterpret_cast(sqlite3_column_text(conditionStmt, 2))); condition.sortOrder = sqlite3_column_int(conditionStmt, 3); outConditions.push_back(condition); } return true; } template bool ReadGroundBorderActions( int64_t caseId, sqlite3_stmt* actionStmt, std::vector &outActions, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(actionStmt); sqlite3_clear_bindings(actionStmt); sqlite3_bind_int64(actionStmt, 1, caseId); for (;;) { const int actionRc = sqlite3_step(actionStmt); if (actionRc == SQLITE_DONE) { break; } if (actionRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read ground border case actions"); } GroundBorderCaseActionRecord action; action.actionType = ToWxString(reinterpret_cast(sqlite3_column_text(actionStmt, 0))); action.targetValue = sqlite3_column_int(actionStmt, 1); action.edge = ToWxString(reinterpret_cast(sqlite3_column_text(actionStmt, 2))); action.replacementValue = sqlite3_column_int(actionStmt, 3); action.sortOrder = sqlite3_column_int(actionStmt, 4); outActions.push_back(action); } return true; } template bool ReadGroundBorderCases( sqlite3_stmt* caseStmt, sqlite3_stmt* conditionStmt, sqlite3_stmt* actionStmt, std::vector &outCases, SetErrorFn &&setErrorFromDatabase ) { for (;;) { const int caseRc = sqlite3_step(caseStmt); if (caseRc == SQLITE_DONE) { break; } if (caseRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read ground border cases"); } const int64_t caseId = sqlite3_column_int64(caseStmt, 0); GroundBorderCaseRecord caseRecord; caseRecord.sortOrder = sqlite3_column_int(caseStmt, 1); if (!ReadGroundBorderConditions(caseId, conditionStmt, caseRecord.conditions, setErrorFromDatabase)) { return false; } if (!ReadGroundBorderActions(caseId, actionStmt, caseRecord.actions, setErrorFromDatabase)) { return false; } outCases.push_back(caseRecord); } return true; } template bool WriteDoodadSingleItems( int64_t alternativeId, const std::vector &singleItems, sqlite3_stmt* insertSingleStmt, SetErrorFn &&setErrorFromDatabase ) { for (const DoodadSingleItemRecord &single : singleItems) { sqlite3_reset(insertSingleStmt); sqlite3_clear_bindings(insertSingleStmt); sqlite3_bind_int64(insertSingleStmt, 1, alternativeId); sqlite3_bind_int(insertSingleStmt, 2, single.itemId); sqlite3_bind_int(insertSingleStmt, 3, single.chance); sqlite3_bind_int(insertSingleStmt, 4, single.sortOrder); if (sqlite3_step(insertSingleStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert doodad single item"); } } return true; } template bool WriteDoodadCompositeTileItems( int64_t tileId, const std::vector &items, sqlite3_stmt* insertTileItemStmt, SetErrorFn &&setErrorFromDatabase ) { for (const DoodadCompositeTileItemRecord &item : items) { sqlite3_reset(insertTileItemStmt); sqlite3_clear_bindings(insertTileItemStmt); sqlite3_bind_int64(insertTileItemStmt, 1, tileId); sqlite3_bind_int(insertTileItemStmt, 2, item.itemId); sqlite3_bind_int(insertTileItemStmt, 3, item.sortOrder); if (sqlite3_step(insertTileItemStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert doodad composite tile item"); } } return true; } template bool WriteDoodadCompositeTiles( sqlite3* connection, int64_t compositeId, const std::vector &tiles, sqlite3_stmt* insertTileStmt, sqlite3_stmt* insertTileItemStmt, SetErrorFn &&setErrorFromDatabase ) { for (const DoodadCompositeTileRecord &tile : tiles) { sqlite3_reset(insertTileStmt); sqlite3_clear_bindings(insertTileStmt); sqlite3_bind_int64(insertTileStmt, 1, compositeId); sqlite3_bind_int(insertTileStmt, 2, tile.offsetX); sqlite3_bind_int(insertTileStmt, 3, tile.offsetY); sqlite3_bind_int(insertTileStmt, 4, tile.offsetZ); sqlite3_bind_int(insertTileStmt, 5, tile.sortOrder); if (sqlite3_step(insertTileStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert doodad composite tile"); } const int64_t tileId = sqlite3_last_insert_rowid(connection); if (!WriteDoodadCompositeTileItems(tileId, tile.items, insertTileItemStmt, setErrorFromDatabase)) { return false; } } return true; } template bool WriteDoodadComposites( sqlite3* connection, int64_t alternativeId, const std::vector &composites, sqlite3_stmt* insertCompositeStmt, sqlite3_stmt* insertTileStmt, sqlite3_stmt* insertTileItemStmt, SetErrorFn &&setErrorFromDatabase ) { for (const DoodadCompositeRecord &composite : composites) { sqlite3_reset(insertCompositeStmt); sqlite3_clear_bindings(insertCompositeStmt); sqlite3_bind_int64(insertCompositeStmt, 1, alternativeId); sqlite3_bind_int(insertCompositeStmt, 2, composite.chance); sqlite3_bind_int(insertCompositeStmt, 3, composite.sortOrder); if (sqlite3_step(insertCompositeStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert doodad composite"); } const int64_t compositeId = sqlite3_last_insert_rowid(connection); if (!WriteDoodadCompositeTiles(connection, compositeId, composite.tiles, insertTileStmt, insertTileItemStmt, setErrorFromDatabase)) { return false; } } return true; } template bool ReadDoodadSingleItems( int64_t alternativeId, sqlite3_stmt* singleStmt, std::vector &outSingleItems, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(singleStmt); sqlite3_clear_bindings(singleStmt); sqlite3_bind_int64(singleStmt, 1, alternativeId); for (;;) { const int singleRc = sqlite3_step(singleStmt); if (singleRc == SQLITE_DONE) { break; } if (singleRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read doodad single items"); } DoodadSingleItemRecord item; item.itemId = sqlite3_column_int(singleStmt, 0); item.chance = sqlite3_column_int(singleStmt, 1); item.sortOrder = sqlite3_column_int(singleStmt, 2); outSingleItems.push_back(item); } return true; } template bool ReadDoodadCompositeTileItems( int64_t tileId, sqlite3_stmt* tileItemStmt, std::vector &outItems, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(tileItemStmt); sqlite3_clear_bindings(tileItemStmt); sqlite3_bind_int64(tileItemStmt, 1, tileId); for (;;) { const int tileItemRc = sqlite3_step(tileItemStmt); if (tileItemRc == SQLITE_DONE) { break; } if (tileItemRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read doodad composite tile items"); } DoodadCompositeTileItemRecord item; item.itemId = sqlite3_column_int(tileItemStmt, 0); item.sortOrder = sqlite3_column_int(tileItemStmt, 1); outItems.push_back(item); } return true; } template bool ReadDoodadCompositeTiles( int64_t compositeId, sqlite3_stmt* tileStmt, sqlite3_stmt* tileItemStmt, std::vector &outTiles, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(tileStmt); sqlite3_clear_bindings(tileStmt); sqlite3_bind_int64(tileStmt, 1, compositeId); for (;;) { const int tileRc = sqlite3_step(tileStmt); if (tileRc == SQLITE_DONE) { break; } if (tileRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read doodad composite tiles"); } const int64_t tileId = sqlite3_column_int64(tileStmt, 0); DoodadCompositeTileRecord tile; tile.offsetX = sqlite3_column_int(tileStmt, 1); tile.offsetY = sqlite3_column_int(tileStmt, 2); tile.offsetZ = sqlite3_column_int(tileStmt, 3); tile.sortOrder = sqlite3_column_int(tileStmt, 4); if (!ReadDoodadCompositeTileItems(tileId, tileItemStmt, tile.items, setErrorFromDatabase)) { return false; } outTiles.push_back(tile); } return true; } template bool ReadDoodadComposites( int64_t alternativeId, sqlite3_stmt* compositeStmt, sqlite3_stmt* tileStmt, sqlite3_stmt* tileItemStmt, std::vector &outComposites, SetErrorFn &&setErrorFromDatabase ) { sqlite3_reset(compositeStmt); sqlite3_clear_bindings(compositeStmt); sqlite3_bind_int64(compositeStmt, 1, alternativeId); for (;;) { const int compositeRc = sqlite3_step(compositeStmt); if (compositeRc == SQLITE_DONE) { break; } if (compositeRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to read doodad composites"); } const int64_t compositeId = sqlite3_column_int64(compositeStmt, 0); DoodadCompositeRecord composite; composite.chance = sqlite3_column_int(compositeStmt, 1); composite.sortOrder = sqlite3_column_int(compositeStmt, 2); if (!ReadDoodadCompositeTiles(compositeId, tileStmt, tileItemStmt, composite.tiles, setErrorFromDatabase)) { return false; } outComposites.push_back(composite); } return true; } template bool ResolveTilesetBrushReference( sqlite3_stmt* findBrushStmt, const TilesetEntryRecord &entry, int64_t &resolvedBrushId, SetErrorFn &&setErrorFromDatabase ) { resolvedBrushId = entry.brushId; if (resolvedBrushId != 0 || entry.brushName.IsEmpty()) { return true; } sqlite3_reset(findBrushStmt); sqlite3_clear_bindings(findBrushStmt); sqlite3_bind_text(findBrushStmt, 1, entry.brushName.utf8_str(), -1, SQLITE_TRANSIENT); const int firstRc = sqlite3_step(findBrushStmt); if (firstRc == SQLITE_DONE) { return true; } if (firstRc != SQLITE_ROW) { return setErrorFromDatabase("Failed to resolve tileset brush reference"); } resolvedBrushId = sqlite3_column_int64(findBrushStmt, 0); const int secondRc = sqlite3_step(findBrushStmt); if (secondRc == SQLITE_ROW) { resolvedBrushId = 0; return true; } if (secondRc != SQLITE_DONE) { return setErrorFromDatabase("Failed to resolve tileset brush reference"); } return true; } template bool WriteTilesetEntries( int64_t sectionId, const std::vector &entries, sqlite3_stmt* insertEntryStmt, sqlite3_stmt* findBrushStmt, SetErrorFn &&setErrorFromDatabase ) { for (const TilesetEntryRecord &entry : entries) { int64_t resolvedBrushId = 0; if (!ResolveTilesetBrushReference(findBrushStmt, entry, resolvedBrushId, setErrorFromDatabase)) { return false; } sqlite3_reset(insertEntryStmt); sqlite3_clear_bindings(insertEntryStmt); sqlite3_bind_int64(insertEntryStmt, 1, sectionId); sqlite3_bind_text(insertEntryStmt, 2, entry.entryKind.utf8_str(), -1, SQLITE_TRANSIENT); if (resolvedBrushId != 0) { sqlite3_bind_int64(insertEntryStmt, 3, resolvedBrushId); } else { sqlite3_bind_null(insertEntryStmt, 3); } sqlite3_bind_text(insertEntryStmt, 4, entry.brushName.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertEntryStmt, 5, entry.itemId); sqlite3_bind_int(insertEntryStmt, 6, entry.fromItemId); sqlite3_bind_int(insertEntryStmt, 7, entry.toItemId); sqlite3_bind_text(insertEntryStmt, 8, entry.afterBrushName.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertEntryStmt, 9, entry.afterItemId); sqlite3_bind_int(insertEntryStmt, 10, entry.sortOrder); if (sqlite3_step(insertEntryStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert tileset entry"); } } return true; } template bool WriteWallPartItems( int64_t wallPartId, const std::vector &items, sqlite3_stmt* insertItemStmt, SetErrorFn &&setErrorFromDatabase ) { for (const WallPartItemRecord &item : items) { sqlite3_reset(insertItemStmt); sqlite3_clear_bindings(insertItemStmt); sqlite3_bind_int64(insertItemStmt, 1, wallPartId); sqlite3_bind_int(insertItemStmt, 2, item.itemId); sqlite3_bind_int(insertItemStmt, 3, item.chance); sqlite3_bind_int(insertItemStmt, 4, item.sortOrder); if (sqlite3_step(insertItemStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert wall part item"); } } return true; } template bool WriteWallPartDoors( int64_t wallPartId, const std::vector &doors, sqlite3_stmt* insertDoorStmt, SetErrorFn &&setErrorFromDatabase ) { for (const WallPartDoorRecord &door : doors) { sqlite3_reset(insertDoorStmt); sqlite3_clear_bindings(insertDoorStmt); sqlite3_bind_int64(insertDoorStmt, 1, wallPartId); sqlite3_bind_int(insertDoorStmt, 2, door.itemId); sqlite3_bind_text(insertDoorStmt, 3, door.doorType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertDoorStmt, 4, door.isOpen ? 1 : 0); sqlite3_bind_int(insertDoorStmt, 5, door.wallHateMe ? 1 : 0); sqlite3_bind_int(insertDoorStmt, 6, door.sortOrder); if (sqlite3_step(insertDoorStmt) != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert wall part door"); } } return true; } } // namespace BrushDatabaseSession::BrushDatabaseSession() = default; BrushDatabaseSession::~BrushDatabaseSession() { close(); } BrushDatabaseComponent::BrushDatabaseComponent(BrushDatabaseSession &session) : session_(session), connection_(session.connection_), lastError_(session.lastError_), readOnly_(session.readOnly_) { } BrushDatabaseSchemaManager::BrushDatabaseSchemaManager(BrushDatabaseSession &session) : BrushDatabaseComponent(session) { } BrushDatabaseBrushRepository::BrushDatabaseBrushRepository(BrushDatabaseSession &session) : BrushDatabaseComponent(session) { } BrushDatabaseCatalogRepository::BrushDatabaseCatalogRepository(BrushDatabaseSession &session, BrushDatabaseSchemaManager &schemaManager) : BrushDatabaseComponent(session), schemaManager_(schemaManager) { } BrushDatabase::~BrushDatabase() = default; bool BrushDatabase::initialize(const wxString &databasePath) { if (!session_.open(databasePath)) { return false; } return schemaManager_.initializeSchema(); } bool BrushDatabase::openReadOnly(const wxString &databasePath) { return session_.openReadOnly(databasePath); } bool BrushDatabase::open(const wxString &databasePath) { return session_.open(databasePath); } void BrushDatabase::close() { session_.close(); } bool BrushDatabase::isOpen() const { return session_.isOpen(); } bool BrushDatabase::isReadOnly() const { return session_.isReadOnly(); } const wxString &BrushDatabase::getDatabasePath() const { return session_.getDatabasePath(); } const wxString &BrushDatabase::getLastError() const { return session_.getLastError(); } bool BrushDatabase::testDatabaseConnection() { return session_.testDatabaseConnection(); } bool BrushDatabase::upsertBrush(const BrushRecord &brush, int64_t &brushId) { return brushRepository_.upsertBrush(brush, brushId); } bool BrushDatabase::listBrushesByType(const wxString &type, std::vector &outBrushes) { return brushRepository_.listBrushesByType(type, outBrushes); } bool BrushDatabase::getCompleteBrushById(int64_t brushId, BrushStorageRecord &outBrush) { return brushRepository_.getCompleteBrushById(brushId, outBrush); } bool BrushDatabase::deleteBrushesByType(const wxString &type) { return brushRepository_.deleteBrushesByType(type); } bool BrushDatabase::replaceBrushItems(int64_t brushId, const std::vector &items) { return brushRepository_.replaceBrushItems(brushId, items); } bool BrushDatabase::upsertBorderSet(const BorderSetRecord &borderSet, int64_t &borderSetId) { return brushRepository_.upsertBorderSet(borderSet, borderSetId); } bool BrushDatabase::findBorderSetByXmlBorderId(int xmlBorderId, BorderSetRecord &outBorderSet) { return brushRepository_.findBorderSetByXmlBorderId(xmlBorderId, outBorderSet); } bool BrushDatabase::replaceBorderSetItems(int64_t borderSetId, const std::vector &items) { return brushRepository_.replaceBorderSetItems(borderSetId, items); } bool BrushDatabase::deleteBorderSetsByScope(const wxString &borderScope) { return brushRepository_.deleteBorderSetsByScope(borderScope); } bool BrushDatabase::deleteOwnedBorderSetsForBrush(int64_t brushId) { return brushRepository_.deleteOwnedBorderSetsForBrush(brushId); } bool BrushDatabase::replaceGroundBrushBorders(int64_t brushId, const std::vector &borders) { return brushRepository_.replaceGroundBrushBorders(brushId, borders); } bool BrushDatabase::replaceBrushLinks(int64_t brushId, const std::vector &links) { return brushRepository_.replaceBrushLinks(brushId, links); } bool BrushDatabase::replaceWallParts(int64_t brushId, const std::vector &parts) { return brushRepository_.replaceWallParts(brushId, parts); } bool BrushDatabase::replaceCarpetNodes(int64_t brushId, const std::vector &nodes) { return brushRepository_.replaceCarpetNodes(brushId, nodes); } bool BrushDatabase::replaceTableNodes(int64_t brushId, const std::vector &nodes) { return brushRepository_.replaceTableNodes(brushId, nodes); } bool BrushDatabase::replaceDoodadAlternatives(int64_t brushId, const std::vector &alternatives) { return brushRepository_.replaceDoodadAlternatives(brushId, alternatives); } bool BrushDatabase::resolveGroundReferenceNames() { return brushRepository_.resolveGroundReferenceNames(); } bool BrushDatabase::replaceAllTilesets(const std::vector &tilesets) { return catalogRepository_.replaceAllTilesets(tilesets); } bool BrushDatabase::getAllTilesets(std::vector &outTilesets) { return catalogRepository_.getAllTilesets(outTilesets); } bool BrushDatabase::generateAuditReport(MaterialsDatabaseAuditReport &outReport) { return catalogRepository_.generateAuditReport(outReport); } bool BrushDatabase::hasCompleteImportForCurrentSchema(bool &outReady) { return catalogRepository_.hasCompleteImportForCurrentSchema(outReady); } int BrushDatabase::getExpectedSchemaVersion() const { return catalogRepository_.getExpectedSchemaVersion(); } bool BrushDatabaseSession::initialize(const wxString &databasePath) { return open(databasePath); } bool BrushDatabaseSession::openReadOnly(const wxString &databasePath) { return openInternal(databasePath, true); } bool BrushDatabaseSession::open(const wxString &databasePath) { return openInternal(databasePath, false); } bool BrushDatabaseSession::openInternal(const wxString &databasePath, bool readOnly) { if (connection_ && databasePath_ == databasePath) { if (readOnly_ == readOnly) { return true; } close(); } if (connection_) { close(); } if (!readOnly) { wxFileName dbFile(databasePath); if (!dbFile.DirExists() && !dbFile.Mkdir(0755, wxPATH_MKDIR_FULL)) { return setError("Failed to create SQLite directory: " + dbFile.GetPath()); } } wxFileName dbFile(databasePath); const int flags = readOnly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); const int rc = sqlite3_open_v2(dbFile.GetFullPath().utf8_str(), &connection_, flags, nullptr); if (rc != SQLITE_OK) { const wxString dbError = ToWxString(sqlite3_errmsg(connection_)); close(); return setError("Failed to open SQLite database: " + dbError); } databasePath_ = dbFile.GetFullPath(); lastError_.clear(); readOnly_ = readOnly; if (!execute("PRAGMA foreign_keys = ON;")) { close(); return false; } if (!execute("PRAGMA busy_timeout = 3000;")) { close(); return false; } if (!readOnly) { if (!execute("PRAGMA journal_mode = WAL;")) { close(); return false; } } else if (!execute("PRAGMA query_only = ON;")) { close(); return false; } spdlog::info("SQLite brush database opened ({}): {}", readOnly ? "read-only" : "read-write", databasePath_.ToStdString()); return true; } void BrushDatabaseSession::close() { if (connection_) { sqlite3_close(connection_); connection_ = nullptr; } databasePath_.clear(); readOnly_ = false; transactionDepth_ = 0; nextSavepointId_ = 0; savepointIds_.clear(); } bool BrushDatabaseSession::isOpen() const { return connection_ != nullptr; } bool BrushDatabaseSession::isReadOnly() const { return readOnly_; } const wxString &BrushDatabaseSession::getDatabasePath() const { return databasePath_; } const wxString &BrushDatabaseSession::getLastError() const { return lastError_; } sqlite3* BrushDatabaseSession::connection() const { return connection_; } sqlite3* BrushDatabaseComponent::connection() const { return session_.connection(); } bool BrushDatabaseComponent::isOpen() const { return session_.isOpen(); } bool BrushDatabaseComponent::isReadOnly() const { return session_.isReadOnly(); } const wxString &BrushDatabaseComponent::lastError() const { return session_.getLastError(); } bool BrushDatabaseComponent::execute(const wxString &sql) { return session_.execute(sql); } bool BrushDatabaseComponent::prepare(const char* sql, sqlite3_stmt** stmt) { return session_.prepare(sql, stmt); } bool BrushDatabaseComponent::beginTransaction() { return session_.beginTransaction(); } bool BrushDatabaseComponent::commitTransaction() { return session_.commitTransaction(); } bool BrushDatabaseComponent::rollbackTransaction() { return session_.rollbackTransaction(); } bool BrushDatabaseComponent::setError(const wxString &message) { return session_.setError(message); } bool BrushDatabaseComponent::setErrorFromDatabase(const wxString &prefix) { return session_.setErrorFromDatabase(prefix); } bool BrushDatabaseSchemaManager::ensureSchemaVersionTable() { if (!execute("CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);")) { return false; } return execute("INSERT INTO schema_version(version) " "SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM schema_version);"); } bool BrushDatabaseSchemaManager::getCurrentSchemaVersion(int &version) { version = 0; sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT version FROM schema_version LIMIT 1;", &stmt)) { return false; } const int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { version = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return true; } sqlite3_finalize(stmt); if (rc == SQLITE_DONE) { return setError("SQLite schema version row was not found."); } return setErrorFromDatabase("Failed to query SQLite schema version"); } bool BrushDatabaseSchemaManager::setSchemaVersion(int version) { sqlite3_stmt* stmt = nullptr; if (!prepare("UPDATE schema_version SET version = ?;", &stmt)) { return false; } sqlite3_bind_int(stmt, 1, version); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to update SQLite schema version"); } return true; } bool BrushDatabaseSchemaManager::columnExists(const wxString &tableName, const wxString &columnName, bool &exists) { exists = false; sqlite3_stmt* stmt = nullptr; const wxString sql = "PRAGMA table_info(" + tableName + ");"; if (!prepare(sql.utf8_str(), &stmt)) { return false; } while (!exists) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { break; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to inspect SQLite table info"); } const wxString currentName = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 1))); if (currentName == columnName) { exists = true; } } sqlite3_finalize(stmt); return true; } bool BrushDatabaseSchemaManager::migrateToVersion1() { if (!execute("CREATE TABLE IF NOT EXISTS brushes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "type TEXT NOT NULL," "look_id INTEGER NOT NULL DEFAULT 0," "z_order INTEGER NOT NULL DEFAULT 0," "created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," "updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP" ");")) { return false; } if (!execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_brushes_type_name " "ON brushes(type, name);")) { return false; } if (!execute("CREATE TABLE IF NOT EXISTS brush_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");")) { return false; } if (!execute("CREATE INDEX IF NOT EXISTS idx_brush_items_item_id " "ON brush_items(item_id);")) { return false; } if (!execute("CREATE INDEX IF NOT EXISTS idx_brush_items_brush_sort " "ON brush_items(brush_id, sort_order);")) { return false; } if (!execute("CREATE TABLE IF NOT EXISTS ground_borders (" "brush_id INTEGER NOT NULL," "border_id INTEGER NOT NULL," "align TEXT NOT NULL DEFAULT 'outer'," "to_brush_id INTEGER," "PRIMARY KEY (brush_id, border_id)," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE," "FOREIGN KEY (to_brush_id) REFERENCES brushes(id) ON DELETE SET NULL" ");")) { return false; } if (!execute("CREATE TABLE IF NOT EXISTS ground_optional_borders (" "brush_id INTEGER NOT NULL," "border_id INTEGER NOT NULL," "PRIMARY KEY (brush_id, border_id)," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");")) { return false; } if (!execute("CREATE TABLE IF NOT EXISTS brush_relationships (" "from_brush_id INTEGER NOT NULL," "to_brush_id INTEGER NOT NULL," "relationship_type TEXT NOT NULL," "PRIMARY KEY (from_brush_id, to_brush_id, relationship_type)," "FOREIGN KEY (from_brush_id) REFERENCES brushes(id) ON DELETE CASCADE," "FOREIGN KEY (to_brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");")) { return false; } return execute("CREATE INDEX IF NOT EXISTS idx_brush_relationships_to " "ON brush_relationships(to_brush_id, relationship_type);"); } bool BrushDatabaseSchemaManager::addColumnIfMissing(const wxString &tableName, const wxString &columnName, const wxString &definition) { bool exists = false; if (!columnExists(tableName, columnName, exists)) { return false; } if (exists) { return true; } return execute("ALTER TABLE " + tableName + " ADD COLUMN " + definition + ";"); } bool BrushDatabaseSchemaManager::executeStatements(std::initializer_list statements) { for (const char* statement : statements) { if (!execute(statement)) { return false; } } return true; } bool BrushDatabaseSchemaManager::addVersion2BrushColumns() { struct ColumnDefinition { const char* name; const char* definition; }; static constexpr std::array kBrushColumns = { { { "source_file", "source_file TEXT NOT NULL DEFAULT ''" }, { "server_look_id", "server_look_id INTEGER NOT NULL DEFAULT 0" }, { "draggable", "draggable INTEGER NOT NULL DEFAULT 0 CHECK(draggable IN (0, 1))" }, { "on_blocking", "on_blocking INTEGER NOT NULL DEFAULT 0 CHECK(on_blocking IN (0, 1))" }, { "on_duplicate", "on_duplicate INTEGER NOT NULL DEFAULT 0 CHECK(on_duplicate IN (0, 1))" }, { "redo_borders", "redo_borders INTEGER NOT NULL DEFAULT 0 CHECK(redo_borders IN (0, 1))" }, { "randomize", "randomize INTEGER NOT NULL DEFAULT 0 CHECK(randomize IN (0, 1))" }, { "one_size", "one_size INTEGER NOT NULL DEFAULT 0 CHECK(one_size IN (0, 1))" }, { "solo_optional", "solo_optional INTEGER NOT NULL DEFAULT 0 CHECK(solo_optional IN (0, 1))" }, { "thickness", "thickness INTEGER NOT NULL DEFAULT 0" }, { "thickness_ceiling", "thickness_ceiling INTEGER NOT NULL DEFAULT 0" }, } }; for (const ColumnDefinition &column : kBrushColumns) { if (!addColumnIfMissing("brushes", column.name, column.definition)) { return false; } } return executeStatements({ "CREATE INDEX IF NOT EXISTS idx_brushes_name ON brushes(name);", }); } bool BrushDatabaseSchemaManager::createVersion2BorderSchema() { return executeStatements({ "CREATE TABLE IF NOT EXISTS border_sets (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "xml_border_id INTEGER UNIQUE," "owner_brush_id INTEGER," "border_scope TEXT NOT NULL DEFAULT 'global'," "border_type TEXT NOT NULL DEFAULT 'normal'," "border_group INTEGER NOT NULL DEFAULT 0," "ground_equivalent INTEGER NOT NULL DEFAULT 0," "source_file TEXT NOT NULL DEFAULT ''," "created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," "updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," "FOREIGN KEY (owner_brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");", "CREATE INDEX IF NOT EXISTS idx_border_sets_owner_scope " "ON border_sets(owner_brush_id, border_scope);", "CREATE TABLE IF NOT EXISTS border_set_items (" "border_set_id INTEGER NOT NULL," "edge TEXT NOT NULL," "item_id INTEGER NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "PRIMARY KEY (border_set_id, edge, item_id)," "FOREIGN KEY (border_set_id) REFERENCES border_sets(id) ON DELETE CASCADE" ");", "CREATE INDEX IF NOT EXISTS idx_border_set_items_item " "ON border_set_items(item_id);", "CREATE TABLE IF NOT EXISTS ground_brush_borders (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "border_set_id INTEGER NOT NULL," "border_role TEXT NOT NULL DEFAULT 'normal'," "align TEXT NOT NULL DEFAULT 'outer'," "target_mode TEXT NOT NULL DEFAULT 'all'," "target_brush_id INTEGER," "target_brush_name TEXT NOT NULL DEFAULT ''," "super_border INTEGER NOT NULL DEFAULT 0 CHECK(super_border IN (0, 1))," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE," "FOREIGN KEY (border_set_id) REFERENCES border_sets(id) ON DELETE CASCADE," "FOREIGN KEY (target_brush_id) REFERENCES brushes(id) ON DELETE SET NULL" ");", "CREATE INDEX IF NOT EXISTS idx_ground_brush_borders_brush " "ON ground_brush_borders(brush_id, border_role, align, sort_order);", "CREATE INDEX IF NOT EXISTS idx_ground_brush_borders_target " "ON ground_brush_borders(target_brush_name, target_mode);", "CREATE TABLE IF NOT EXISTS ground_border_cases (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "ground_brush_border_id INTEGER NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (ground_brush_border_id) REFERENCES ground_brush_borders(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS ground_border_case_conditions (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "ground_border_case_id INTEGER NOT NULL," "condition_type TEXT NOT NULL," "match_value INTEGER NOT NULL DEFAULT 0," "edge TEXT NOT NULL DEFAULT ''," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (ground_border_case_id) REFERENCES ground_border_cases(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS ground_border_case_actions (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "ground_border_case_id INTEGER NOT NULL," "action_type TEXT NOT NULL," "target_value INTEGER NOT NULL DEFAULT 0," "edge TEXT NOT NULL DEFAULT ''," "replacement_value INTEGER NOT NULL DEFAULT 0," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (ground_border_case_id) REFERENCES ground_border_cases(id) ON DELETE CASCADE" ");", }); } bool BrushDatabaseSchemaManager::createVersion2BrushDetailSchema() { return executeStatements({ "CREATE TABLE IF NOT EXISTS brush_links (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "target_brush_id INTEGER," "target_brush_name TEXT NOT NULL DEFAULT ''," "relation_type TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE," "FOREIGN KEY (target_brush_id) REFERENCES brushes(id) ON DELETE SET NULL" ");", "CREATE INDEX IF NOT EXISTS idx_brush_links_type " "ON brush_links(brush_id, relation_type, sort_order);", "CREATE INDEX IF NOT EXISTS idx_brush_links_target_name " "ON brush_links(target_brush_name);", "CREATE TABLE IF NOT EXISTS wall_parts (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "part_type TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE," "UNIQUE (brush_id, part_type)" ");", "CREATE TABLE IF NOT EXISTS wall_part_items (" "wall_part_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "PRIMARY KEY (wall_part_id, item_id)," "FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS wall_part_doors (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "wall_part_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "door_type TEXT NOT NULL DEFAULT 'normal'," "is_open INTEGER NOT NULL DEFAULT 0 CHECK(is_open IN (0, 1))," "wall_hate_me INTEGER NOT NULL DEFAULT 0 CHECK(wall_hate_me IN (0, 1))," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE" ");", "CREATE INDEX IF NOT EXISTS idx_wall_part_doors_part " "ON wall_part_doors(wall_part_id, sort_order);", "CREATE TABLE IF NOT EXISTS carpet_nodes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "align TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS carpet_node_items (" "carpet_node_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "PRIMARY KEY (carpet_node_id, item_id)," "FOREIGN KEY (carpet_node_id) REFERENCES carpet_nodes(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS table_nodes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "align TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS table_node_items (" "table_node_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "PRIMARY KEY (table_node_id, item_id)," "FOREIGN KEY (table_node_id) REFERENCES table_nodes(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS doodad_alternatives (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS doodad_single_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_alternative_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS doodad_composites (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_alternative_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS doodad_composite_tiles (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_composite_id INTEGER NOT NULL," "offset_x INTEGER NOT NULL DEFAULT 0," "offset_y INTEGER NOT NULL DEFAULT 0," "offset_z INTEGER NOT NULL DEFAULT 0," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_composite_id) REFERENCES doodad_composites(id) ON DELETE CASCADE" ");", "CREATE TABLE IF NOT EXISTS doodad_composite_tile_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_composite_tile_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_composite_tile_id) REFERENCES doodad_composite_tiles(id) ON DELETE CASCADE" ");", }); } bool BrushDatabaseSchemaManager::createVersion2TilesetSchema() { return executeStatements({ "CREATE TABLE IF NOT EXISTS tilesets (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL UNIQUE," "source_file TEXT NOT NULL DEFAULT ''," "created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," "updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP" ");", "CREATE TABLE IF NOT EXISTS tileset_sections (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "tileset_id INTEGER NOT NULL," "section_type TEXT NOT NULL," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (tileset_id) REFERENCES tilesets(id) ON DELETE CASCADE," "UNIQUE (tileset_id, section_type)" ");", "CREATE TABLE IF NOT EXISTS tileset_brush_entries (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "tileset_section_id INTEGER NOT NULL," "brush_id INTEGER," "brush_name TEXT NOT NULL DEFAULT ''," "after_brush_name TEXT NOT NULL DEFAULT ''," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (tileset_section_id) REFERENCES tileset_sections(id) ON DELETE CASCADE," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE SET NULL" ");", "CREATE INDEX IF NOT EXISTS idx_tileset_brush_entries_section " "ON tileset_brush_entries(tileset_section_id, sort_order);", "CREATE INDEX IF NOT EXISTS idx_tileset_brush_entries_name " "ON tileset_brush_entries(brush_name);", }); } bool BrushDatabaseSchemaManager::migrateToVersion2() { return addVersion2BrushColumns() && createVersion2BorderSchema() && createVersion2BrushDetailSchema() && createVersion2TilesetSchema(); } template bool BrushDatabaseSchemaManager::applySchemaMigrationStep(int &version, int targetVersion) { if (version >= targetVersion) { return true; } if (!(this->*Migration)()) { return false; } if (!setSchemaVersion(targetVersion)) { return false; } version = targetVersion; return true; } bool BrushDatabaseSchemaManager::initializeSchema() { if (!isOpen()) { return setError("SQLite database is not open."); } if (readOnly_) { return setError("SQLite schema initialization requires a read-write connection."); } if (!beginTransaction()) { return false; } const auto rollback = [this]() { rollbackTransaction(); return false; }; if (!ensureSchemaVersionTable()) { return rollback(); } int version = 0; if (!getCurrentSchemaVersion(version)) { return rollback(); } if (version > kBrushDatabaseSchemaVersion) { rollbackTransaction(); return setError(wxString::Format("SQLite schema version %d is newer than supported version %d.", version, kBrushDatabaseSchemaVersion)); } if (!applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion1>(version, 1) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion2>(version, 2) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion3>(version, 3) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion4>(version, 4) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion5>(version, 5) || !applySchemaMigrationStep<&BrushDatabaseSchemaManager::migrateToVersion6>(version, 6)) { return rollback(); } if (!commitTransaction()) { return rollback(); } spdlog::info("SQLite brush database schema initialized at version {}", version); return true; } bool BrushDatabaseSchemaManager::migrateToVersion3() { const wxString recreateSql = "DROP TABLE IF EXISTS brush_items;" "DROP TABLE IF EXISTS wall_part_items;" "DROP TABLE IF EXISTS carpet_node_items;" "DROP TABLE IF EXISTS table_node_items;" "DROP TABLE IF EXISTS doodad_single_items;" "DROP TABLE IF EXISTS doodad_composites;" "CREATE TABLE brush_items (" "brush_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "PRIMARY KEY (brush_id, item_id)," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");" "CREATE INDEX idx_brush_items_item_id ON brush_items(item_id);" "CREATE TABLE wall_part_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "wall_part_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (wall_part_id) REFERENCES wall_parts(id) ON DELETE CASCADE" ");" "CREATE INDEX idx_wall_part_items_item ON wall_part_items(item_id);" "CREATE TABLE carpet_node_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "carpet_node_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (carpet_node_id) REFERENCES carpet_nodes(id) ON DELETE CASCADE" ");" "CREATE TABLE table_node_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "table_node_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (table_node_id) REFERENCES table_nodes(id) ON DELETE CASCADE" ");" "CREATE TABLE doodad_single_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_alternative_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE" ");" "CREATE TABLE doodad_composites (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "doodad_alternative_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (doodad_alternative_id) REFERENCES doodad_alternatives(id) ON DELETE CASCADE" ");"; if (!execute(recreateSql)) { return false; } return true; } bool BrushDatabaseSchemaManager::migrateToVersion4() { const wxString recreateSql = "DROP TABLE IF EXISTS brush_items;" "CREATE TABLE brush_items (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "brush_id INTEGER NOT NULL," "item_id INTEGER NOT NULL," "chance INTEGER NOT NULL DEFAULT 1 CHECK(chance >= 0)," "sort_order INTEGER NOT NULL DEFAULT 0," "FOREIGN KEY (brush_id) REFERENCES brushes(id) ON DELETE CASCADE" ");" "CREATE INDEX idx_brush_items_item_id ON brush_items(item_id);" "CREATE INDEX idx_brush_items_brush_sort ON brush_items(brush_id, sort_order);"; return execute(recreateSql); } bool BrushDatabaseSchemaManager::migrateToVersion5() { return execute(kRecreateTilesetTablesSql); } bool BrushDatabaseSchemaManager::migrateToVersion6() { return execute(kRecreateTilesetTablesSql); } bool BrushDatabaseSession::testDatabaseConnection() { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT 1;", &stmt)) { return false; } const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_ROW) { return setErrorFromDatabase("SQLite connection test failed"); } return true; } bool BrushDatabaseBrushRepository::testBasicCRUD() { if (!isOpen()) { return setError("SQLite database is not open."); } BrushRecord brush; brush.name = "__sqlite_smoke_test__"; brush.type = "ground"; brush.lookId = 100; brush.zOrder = 3; int64_t insertedId = 0; if (!insertBrush(brush, insertedId)) { return false; } std::vector items = { { insertedId, 100, 100 }, { insertedId, 101, 50 }, }; if (!replaceBrushItems(insertedId, items)) { deleteBrush(insertedId); return false; } BrushRecord loadedBrush; if (!getBrushById(insertedId, loadedBrush)) { deleteBrush(insertedId); return false; } loadedBrush.name = "__sqlite_smoke_test_updated__"; if (!updateBrush(loadedBrush)) { deleteBrush(insertedId); return false; } std::vector loadedItems; if (!getBrushItems(insertedId, loadedItems)) { deleteBrush(insertedId); return false; } if (loadedItems.size() != 2) { deleteBrush(insertedId); return setError("SQLite smoke test failed: unexpected brush item count."); } if (!deleteBrush(insertedId)) { return false; } return true; } bool BrushDatabaseBrushRepository::insertBrush(const BrushRecord &brush, int64_t &insertedId) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("INSERT INTO brushes(" "name, type, look_id, z_order, source_file, server_look_id, " "draggable, on_blocking, on_duplicate, redo_borders, randomize, " "one_size, solo_optional, thickness, thickness_ceiling, updated_at" ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);", &stmt)) { return false; } BindBrushRecordFields(stmt, brush, 1); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert brush"); } insertedId = sqlite3_last_insert_rowid(connection_); return true; } bool BrushDatabaseBrushRepository::upsertBrush(const BrushRecord &brush, int64_t &brushId) { BrushRecord existingBrush; if (findBrushByNameAndType(brush.name, brush.type, existingBrush)) { BrushRecord updatedBrush = brush; updatedBrush.id = existingBrush.id; if (!updateBrush(updatedBrush)) { return false; } brushId = existingBrush.id; return true; } return insertBrush(brush, brushId); } bool BrushDatabaseBrushRepository::getBrushById(int64_t brushId, BrushRecord &outBrush) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE id = ?;").utf8_str(), &stmt)) { return false; } sqlite3_bind_int64(stmt, 1, brushId); const int rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setError(wxString::Format("Brush %lld was not found in SQLite.", static_cast(brushId))); } ReadBrushRecordFromStatement(stmt, outBrush); sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::listBrushesByType(const wxString &type, std::vector &outBrushes) { outBrushes.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE type = ? ORDER BY name ASC, id ASC;").utf8_str(), &stmt)) { return false; } sqlite3_bind_text(stmt, 1, type.utf8_str(), -1, SQLITE_TRANSIENT); for (;;) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { break; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to list brushes by type"); } BrushRecord brush; ReadBrushRecordFromStatement(stmt, brush); outBrushes.push_back(brush); } sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::findBrushByNameAndType(const wxString &name, const wxString &type, BrushRecord &outBrush) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare(("SELECT " + wxString::FromUTF8(kBrushSelectColumns) + " FROM brushes WHERE name = ? AND type = ? LIMIT 1;").utf8_str(), &stmt)) { return false; } sqlite3_bind_text(stmt, 1, name.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, type.utf8_str(), -1, SQLITE_TRANSIENT); const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { sqlite3_finalize(stmt); return false; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); setErrorFromDatabase("Failed to query brush by name and type"); return false; } ReadBrushRecordFromStatement(stmt, outBrush); sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::getCompleteBrushById(int64_t brushId, BrushStorageRecord &outBrush) { outBrush = BrushStorageRecord(); if (!getBrushById(brushId, outBrush.brush)) { return false; } if (!getBrushItems(brushId, outBrush.items)) { return false; } if (!getGroundBrushBorders(brushId, outBrush.borders)) { return false; } if (!getBrushLinks(brushId, outBrush.links)) { return false; } if (!getWallParts(brushId, outBrush.wallParts)) { return false; } if (!getCarpetNodes(brushId, outBrush.carpetNodes)) { return false; } if (!getTableNodes(brushId, outBrush.tableNodes)) { return false; } return getDoodadAlternatives(brushId, outBrush.doodadAlternatives); } bool BrushDatabaseBrushRepository::updateBrush(const BrushRecord &brush) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("UPDATE brushes " "SET name = ?, type = ?, look_id = ?, z_order = ?, source_file = ?, server_look_id = ?, " "draggable = ?, on_blocking = ?, on_duplicate = ?, redo_borders = ?, randomize = ?, " "one_size = ?, solo_optional = ?, thickness = ?, thickness_ceiling = ?, updated_at = CURRENT_TIMESTAMP " "WHERE id = ?;", &stmt)) { return false; } sqlite3_bind_int64(stmt, BindBrushRecordFields(stmt, brush, 1), brush.id); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to update brush"); } return sqlite3_changes(connection_) > 0 || setError(wxString::Format("Brush %lld was not updated.", static_cast(brush.id))); } bool BrushDatabaseBrushRepository::deleteBrush(int64_t brushId) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("DELETE FROM brushes WHERE id = ?;", &stmt)) { return false; } sqlite3_bind_int64(stmt, 1, brushId); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to delete brush"); } return true; } bool BrushDatabaseBrushRepository::deleteBrushesByType(const wxString &type) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("DELETE FROM brushes WHERE type = ?;", &stmt)) { return false; } sqlite3_bind_text(stmt, 1, type.utf8_str(), -1, SQLITE_TRANSIENT); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to delete brushes by type"); } return true; } bool BrushDatabaseBrushRepository::replaceBrushItems(int64_t brushId, const std::vector &items) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM brush_items WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear brush items"); } sqlite3_stmt* insertStmt = nullptr; if (!prepare("INSERT INTO brush_items(brush_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertStmt)) { rollbackTransaction(); return false; } int sortOrder = 0; for (const BrushItemRecord &item : items) { sqlite3_reset(insertStmt); sqlite3_clear_bindings(insertStmt); sqlite3_bind_int64(insertStmt, 1, brushId); sqlite3_bind_int(insertStmt, 2, item.itemId); sqlite3_bind_int(insertStmt, 3, item.chance); sqlite3_bind_int(insertStmt, 4, item.sortOrder != 0 ? item.sortOrder : sortOrder); rc = sqlite3_step(insertStmt); if (rc != SQLITE_DONE) { sqlite3_finalize(insertStmt); rollbackTransaction(); return setErrorFromDatabase("Failed to insert brush item"); } ++sortOrder; } sqlite3_finalize(insertStmt); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getBrushItems(int64_t brushId, std::vector &outItems) { outItems.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT brush_id, item_id, chance, sort_order " "FROM brush_items WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &stmt)) { return false; } sqlite3_bind_int64(stmt, 1, brushId); for (;;) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { break; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to read brush items"); } BrushItemRecord item; item.brushId = sqlite3_column_int64(stmt, 0); item.itemId = sqlite3_column_int(stmt, 1); item.chance = sqlite3_column_int(stmt, 2); item.sortOrder = sqlite3_column_int(stmt, 3); outItems.push_back(item); } sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::upsertBorderSet(const BorderSetRecord &borderSet, int64_t &borderSetId) { if (!isOpen()) { return setError("SQLite database is not open."); } if (borderSet.xmlBorderId > 0) { BorderSetRecord existing; if (findBorderSetByXmlBorderId(borderSet.xmlBorderId, existing)) { sqlite3_stmt* updateStmt = nullptr; if (!prepare("UPDATE border_sets SET owner_brush_id = ?, border_scope = ?, border_type = ?, " "border_group = ?, ground_equivalent = ?, source_file = ?, updated_at = CURRENT_TIMESTAMP " "WHERE id = ?;", &updateStmt)) { return false; } BindNullableInt64(updateStmt, 1, borderSet.ownerBrushId); sqlite3_bind_text(updateStmt, 2, borderSet.borderScope.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(updateStmt, 3, borderSet.borderType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(updateStmt, 4, borderSet.borderGroup); sqlite3_bind_int(updateStmt, 5, borderSet.groundEquivalent); sqlite3_bind_text(updateStmt, 6, borderSet.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(updateStmt, 7, existing.id); const int rc = sqlite3_step(updateStmt); sqlite3_finalize(updateStmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to update border set"); } borderSetId = existing.id; return true; } } sqlite3_stmt* stmt = nullptr; if (!prepare("INSERT INTO border_sets(xml_border_id, owner_brush_id, border_scope, border_type, border_group, ground_equivalent, source_file, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);", &stmt)) { return false; } if (borderSet.xmlBorderId > 0) { sqlite3_bind_int(stmt, 1, borderSet.xmlBorderId); } else { sqlite3_bind_null(stmt, 1); } BindNullableInt64(stmt, 2, borderSet.ownerBrushId); sqlite3_bind_text(stmt, 3, borderSet.borderScope.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, borderSet.borderType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 5, borderSet.borderGroup); sqlite3_bind_int(stmt, 6, borderSet.groundEquivalent); sqlite3_bind_text(stmt, 7, borderSet.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to insert border set"); } borderSetId = sqlite3_last_insert_rowid(connection_); return true; } bool BrushDatabaseBrushRepository::findBorderSetByXmlBorderId(int xmlBorderId, BorderSetRecord &outBorderSet) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT id, xml_border_id, owner_brush_id, border_scope, border_type, border_group, ground_equivalent, source_file " "FROM border_sets WHERE xml_border_id = ? LIMIT 1;", &stmt)) { return false; } sqlite3_bind_int(stmt, 1, xmlBorderId); const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { sqlite3_finalize(stmt); return false; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to query border set by xml id"); } outBorderSet.id = sqlite3_column_int64(stmt, 0); outBorderSet.xmlBorderId = sqlite3_column_int(stmt, 1); outBorderSet.ownerBrushId = ReadNullableInt64(stmt, 2); outBorderSet.borderScope = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 3))); outBorderSet.borderType = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 4))); outBorderSet.borderGroup = sqlite3_column_int(stmt, 5); outBorderSet.groundEquivalent = sqlite3_column_int(stmt, 6); outBorderSet.sourceFile = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 7))); sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::replaceBorderSetItems(int64_t borderSetId, const std::vector &items) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM border_set_items WHERE border_set_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, borderSetId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear border set items"); } sqlite3_stmt* insertStmt = nullptr; if (!prepare("INSERT INTO border_set_items(border_set_id, edge, item_id, sort_order) VALUES (?, ?, ?, ?);", &insertStmt)) { rollbackTransaction(); return false; } for (const BorderSetItemRecord &item : items) { sqlite3_reset(insertStmt); sqlite3_clear_bindings(insertStmt); sqlite3_bind_int64(insertStmt, 1, borderSetId); sqlite3_bind_text(insertStmt, 2, item.edge.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertStmt, 3, item.itemId); sqlite3_bind_int(insertStmt, 4, item.sortOrder); rc = sqlite3_step(insertStmt); if (rc != SQLITE_DONE) { sqlite3_finalize(insertStmt); rollbackTransaction(); return setErrorFromDatabase("Failed to insert border set item"); } } sqlite3_finalize(insertStmt); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::deleteBorderSetsByScope(const wxString &borderScope) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("DELETE FROM border_sets WHERE border_scope = ?;", &stmt)) { return false; } sqlite3_bind_text(stmt, 1, borderScope.utf8_str(), -1, SQLITE_TRANSIENT); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to delete border sets by scope"); } return true; } bool BrushDatabaseBrushRepository::deleteOwnedBorderSetsForBrush(int64_t brushId) { if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("DELETE FROM border_sets WHERE owner_brush_id = ?;", &stmt)) { return false; } sqlite3_bind_int64(stmt, 1, brushId); const int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return setErrorFromDatabase("Failed to delete owned border sets for brush"); } return true; } bool BrushDatabaseBrushRepository::replaceGroundBrushBorders(int64_t brushId, const std::vector &borders) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM ground_brush_borders WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear ground brush borders"); } sqlite3_stmt* insertBorderStmt = nullptr; if (!prepare("INSERT INTO ground_brush_borders(" "brush_id, border_set_id, border_role, align, target_mode, target_brush_id, target_brush_name, super_border, sort_order" ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", &insertBorderStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertCaseStmt = nullptr; if (!prepare("INSERT INTO ground_border_cases(ground_brush_border_id, sort_order) VALUES (?, ?);", &insertCaseStmt)) { sqlite3_finalize(insertBorderStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertConditionStmt = nullptr; if (!prepare("INSERT INTO ground_border_case_conditions(ground_border_case_id, condition_type, match_value, edge, sort_order) " "VALUES (?, ?, ?, ?, ?);", &insertConditionStmt)) { sqlite3_finalize(insertBorderStmt); sqlite3_finalize(insertCaseStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertActionStmt = nullptr; if (!prepare("INSERT INTO ground_border_case_actions(ground_border_case_id, action_type, target_value, edge, replacement_value, sort_order) " "VALUES (?, ?, ?, ?, ?, ?);", &insertActionStmt)) { sqlite3_finalize(insertBorderStmt); sqlite3_finalize(insertCaseStmt); sqlite3_finalize(insertConditionStmt); rollbackTransaction(); return false; } const auto fail = [this, &insertBorderStmt, &insertCaseStmt, &insertConditionStmt, &insertActionStmt](const wxString &message) { FinalizeStatements({ insertBorderStmt, insertCaseStmt, insertConditionStmt, insertActionStmt }); rollbackTransaction(); return setErrorFromDatabase(message); }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (const GroundBrushBorderRecord &border : borders) { sqlite3_reset(insertBorderStmt); sqlite3_clear_bindings(insertBorderStmt); sqlite3_bind_int64(insertBorderStmt, 1, brushId); sqlite3_bind_int64(insertBorderStmt, 2, border.borderSetId); sqlite3_bind_text(insertBorderStmt, 3, border.borderRole.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(insertBorderStmt, 4, border.align.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(insertBorderStmt, 5, border.targetMode.utf8_str(), -1, SQLITE_TRANSIENT); BindNullableInt64(insertBorderStmt, 6, border.targetBrushId); sqlite3_bind_text(insertBorderStmt, 7, border.targetBrushName.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertBorderStmt, 8, border.superBorder ? 1 : 0); sqlite3_bind_int(insertBorderStmt, 9, border.sortOrder); if (sqlite3_step(insertBorderStmt) != SQLITE_DONE) { return fail("Failed to insert ground brush border"); } const int64_t groundBrushBorderId = sqlite3_last_insert_rowid(connection_); if (!WriteGroundBorderCases( connection_, groundBrushBorderId, border.cases, insertCaseStmt, insertConditionStmt, insertActionStmt, setDbError )) { return fail(lastError_); } } FinalizeStatements({ insertBorderStmt, insertCaseStmt, insertConditionStmt, insertActionStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getGroundBrushBorders(int64_t brushId, std::vector &outBorders) { outBorders.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* borderStmt = nullptr; if (!prepare("SELECT id, border_set_id, border_role, align, target_mode, target_brush_id, target_brush_name, super_border, sort_order " "FROM ground_brush_borders WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &borderStmt)) { return false; } sqlite3_stmt* caseStmt = nullptr; if (!prepare("SELECT id, sort_order FROM ground_border_cases " "WHERE ground_brush_border_id = ? ORDER BY sort_order ASC, id ASC;", &caseStmt)) { sqlite3_finalize(borderStmt); return false; } sqlite3_stmt* conditionStmt = nullptr; if (!prepare("SELECT condition_type, match_value, edge, sort_order " "FROM ground_border_case_conditions WHERE ground_border_case_id = ? " "ORDER BY sort_order ASC, id ASC;", &conditionStmt)) { sqlite3_finalize(borderStmt); sqlite3_finalize(caseStmt); return false; } sqlite3_stmt* actionStmt = nullptr; if (!prepare("SELECT action_type, target_value, edge, replacement_value, sort_order " "FROM ground_border_case_actions WHERE ground_border_case_id = ? " "ORDER BY sort_order ASC, id ASC;", &actionStmt)) { sqlite3_finalize(borderStmt); sqlite3_finalize(caseStmt); sqlite3_finalize(conditionStmt); return false; } sqlite3_bind_int64(borderStmt, 1, brushId); const auto fail = [this, &borderStmt, &caseStmt, &conditionStmt, &actionStmt](const wxString &message) { FinalizeStatements({ borderStmt, caseStmt, conditionStmt, actionStmt }); return setErrorFromDatabase(message); }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (;;) { const int borderRc = sqlite3_step(borderStmt); if (borderRc == SQLITE_DONE) { break; } if (borderRc != SQLITE_ROW) { return fail("Failed to read ground brush borders"); } const int64_t groundBrushBorderId = sqlite3_column_int64(borderStmt, 0); GroundBrushBorderRecord border; border.borderSetId = sqlite3_column_int64(borderStmt, 1); border.borderRole = ToWxString(reinterpret_cast(sqlite3_column_text(borderStmt, 2))); border.align = ToWxString(reinterpret_cast(sqlite3_column_text(borderStmt, 3))); border.targetMode = ToWxString(reinterpret_cast(sqlite3_column_text(borderStmt, 4))); border.targetBrushId = ReadNullableInt64(borderStmt, 5); border.targetBrushName = ToWxString(reinterpret_cast(sqlite3_column_text(borderStmt, 6))); border.superBorder = sqlite3_column_int(borderStmt, 7) != 0; border.sortOrder = sqlite3_column_int(borderStmt, 8); sqlite3_reset(caseStmt); sqlite3_clear_bindings(caseStmt); sqlite3_bind_int64(caseStmt, 1, groundBrushBorderId); if (!ReadGroundBorderCases(caseStmt, conditionStmt, actionStmt, border.cases, setDbError)) { return fail(lastError_); } outBorders.push_back(border); } FinalizeStatements({ borderStmt, caseStmt, conditionStmt, actionStmt }); return true; } bool BrushDatabaseBrushRepository::replaceBrushLinks(int64_t brushId, const std::vector &links) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM brush_links WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear brush links"); } sqlite3_stmt* insertStmt = nullptr; if (!prepare("INSERT INTO brush_links(brush_id, target_brush_id, target_brush_name, relation_type, sort_order) VALUES (?, ?, ?, ?, ?);", &insertStmt)) { rollbackTransaction(); return false; } for (const BrushLinkRecord &link : links) { sqlite3_reset(insertStmt); sqlite3_clear_bindings(insertStmt); sqlite3_bind_int64(insertStmt, 1, brushId); BindNullableInt64(insertStmt, 2, link.targetBrushId); sqlite3_bind_text(insertStmt, 3, link.targetBrushName.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(insertStmt, 4, link.relationType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertStmt, 5, link.sortOrder); rc = sqlite3_step(insertStmt); if (rc != SQLITE_DONE) { sqlite3_finalize(insertStmt); rollbackTransaction(); return setErrorFromDatabase("Failed to insert brush link"); } } sqlite3_finalize(insertStmt); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getBrushLinks(int64_t brushId, std::vector &outLinks) { outLinks.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT brush_id, target_brush_id, target_brush_name, relation_type, sort_order " "FROM brush_links WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &stmt)) { return false; } sqlite3_bind_int64(stmt, 1, brushId); for (;;) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { break; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to read brush links"); } BrushLinkRecord link; link.brushId = sqlite3_column_int64(stmt, 0); link.targetBrushId = ReadNullableInt64(stmt, 1); link.targetBrushName = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 2))); link.relationType = ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 3))); link.sortOrder = sqlite3_column_int(stmt, 4); outLinks.push_back(link); } sqlite3_finalize(stmt); return true; } bool BrushDatabaseBrushRepository::replaceWallParts(int64_t brushId, const std::vector &parts) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM wall_parts WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear wall parts"); } sqlite3_stmt* insertPartStmt = nullptr; if (!prepare("INSERT INTO wall_parts(brush_id, part_type, sort_order) VALUES (?, ?, ?);", &insertPartStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertItemStmt = nullptr; if (!prepare("INSERT INTO wall_part_items(wall_part_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) { sqlite3_finalize(insertPartStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertDoorStmt = nullptr; if (!prepare("INSERT INTO wall_part_doors(wall_part_id, item_id, door_type, is_open, wall_hate_me, sort_order) " "VALUES (?, ?, ?, ?, ?, ?);", &insertDoorStmt)) { sqlite3_finalize(insertPartStmt); sqlite3_finalize(insertItemStmt); rollbackTransaction(); return false; } const auto failWithDatabaseError = [this, &insertPartStmt, &insertItemStmt, &insertDoorStmt](const wxString &message) { FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt }); rollbackTransaction(); return setErrorFromDatabase(message); }; const auto fail = [this, &insertPartStmt, &insertItemStmt, &insertDoorStmt]() { FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt }); rollbackTransaction(); return false; }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (const WallPartRecord &part : parts) { sqlite3_reset(insertPartStmt); sqlite3_clear_bindings(insertPartStmt); sqlite3_bind_int64(insertPartStmt, 1, brushId); sqlite3_bind_text(insertPartStmt, 2, part.partType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertPartStmt, 3, part.sortOrder); if (sqlite3_step(insertPartStmt) != SQLITE_DONE) { return failWithDatabaseError("Failed to insert wall part"); } const int64_t wallPartId = sqlite3_last_insert_rowid(connection_); if (!WriteWallPartItems(wallPartId, part.items, insertItemStmt, setDbError)) { return fail(); } if (!WriteWallPartDoors(wallPartId, part.doors, insertDoorStmt, setDbError)) { return fail(); } } FinalizeStatements({ insertPartStmt, insertItemStmt, insertDoorStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getWallParts(int64_t brushId, std::vector &outParts) { outParts.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* partStmt = nullptr; if (!prepare("SELECT id, part_type, sort_order FROM wall_parts " "WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &partStmt)) { return false; } sqlite3_stmt* itemStmt = nullptr; if (!prepare("SELECT item_id, chance, sort_order FROM wall_part_items " "WHERE wall_part_id = ? ORDER BY sort_order ASC, id ASC;", &itemStmt)) { sqlite3_finalize(partStmt); return false; } sqlite3_stmt* doorStmt = nullptr; if (!prepare("SELECT item_id, door_type, is_open, wall_hate_me, sort_order FROM wall_part_doors " "WHERE wall_part_id = ? ORDER BY sort_order ASC, id ASC;", &doorStmt)) { sqlite3_finalize(partStmt); sqlite3_finalize(itemStmt); return false; } sqlite3_bind_int64(partStmt, 1, brushId); for (;;) { const int partRc = sqlite3_step(partStmt); if (partRc == SQLITE_DONE) { break; } if (partRc != SQLITE_ROW) { sqlite3_finalize(partStmt); sqlite3_finalize(itemStmt); sqlite3_finalize(doorStmt); return setErrorFromDatabase("Failed to read wall parts"); } const int64_t wallPartId = sqlite3_column_int64(partStmt, 0); WallPartRecord part; part.partType = ToWxString(reinterpret_cast(sqlite3_column_text(partStmt, 1))); part.sortOrder = sqlite3_column_int(partStmt, 2); sqlite3_reset(itemStmt); sqlite3_clear_bindings(itemStmt); sqlite3_bind_int64(itemStmt, 1, wallPartId); for (;;) { const int itemRc = sqlite3_step(itemStmt); if (itemRc == SQLITE_DONE) { break; } if (itemRc != SQLITE_ROW) { sqlite3_finalize(partStmt); sqlite3_finalize(itemStmt); sqlite3_finalize(doorStmt); return setErrorFromDatabase("Failed to read wall part items"); } WallPartItemRecord item; item.itemId = sqlite3_column_int(itemStmt, 0); item.chance = sqlite3_column_int(itemStmt, 1); item.sortOrder = sqlite3_column_int(itemStmt, 2); part.items.push_back(item); } sqlite3_reset(doorStmt); sqlite3_clear_bindings(doorStmt); sqlite3_bind_int64(doorStmt, 1, wallPartId); for (;;) { const int doorRc = sqlite3_step(doorStmt); if (doorRc == SQLITE_DONE) { break; } if (doorRc != SQLITE_ROW) { sqlite3_finalize(partStmt); sqlite3_finalize(itemStmt); sqlite3_finalize(doorStmt); return setErrorFromDatabase("Failed to read wall part doors"); } WallPartDoorRecord door; door.itemId = sqlite3_column_int(doorStmt, 0); door.doorType = ToWxString(reinterpret_cast(sqlite3_column_text(doorStmt, 1))); door.isOpen = sqlite3_column_int(doorStmt, 2) != 0; door.wallHateMe = sqlite3_column_int(doorStmt, 3) != 0; door.sortOrder = sqlite3_column_int(doorStmt, 4); part.doors.push_back(door); } outParts.push_back(part); } sqlite3_finalize(partStmt); sqlite3_finalize(itemStmt); sqlite3_finalize(doorStmt); return true; } bool BrushDatabaseBrushRepository::replaceCarpetNodes(int64_t brushId, const std::vector &nodes) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM carpet_nodes WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear carpet nodes"); } sqlite3_stmt* insertNodeStmt = nullptr; if (!prepare("INSERT INTO carpet_nodes(brush_id, align, sort_order) VALUES (?, ?, ?);", &insertNodeStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertItemStmt = nullptr; if (!prepare("INSERT INTO carpet_node_items(carpet_node_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) { sqlite3_finalize(insertNodeStmt); rollbackTransaction(); return false; } if (!WriteAlignedNodesWithItems( connection_, brushId, nodes, { insertNodeStmt, insertItemStmt, "Failed to insert carpet node", "Failed to insert carpet node item" }, [this](const wxString &message) { return setErrorFromDatabase(message); } )) { FinalizeStatements({ insertNodeStmt, insertItemStmt }); rollbackTransaction(); return false; } FinalizeStatements({ insertNodeStmt, insertItemStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getCarpetNodes(int64_t brushId, std::vector &outNodes) { outNodes.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* nodeStmt = nullptr; if (!prepare("SELECT id, align, sort_order FROM carpet_nodes " "WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &nodeStmt)) { return false; } sqlite3_stmt* itemStmt = nullptr; if (!prepare("SELECT item_id, chance, sort_order FROM carpet_node_items " "WHERE carpet_node_id = ? ORDER BY sort_order ASC, id ASC;", &itemStmt)) { sqlite3_finalize(nodeStmt); return false; } sqlite3_bind_int64(nodeStmt, 1, brushId); if (!ReadAlignedNodesWithItems( nodeStmt, itemStmt, outNodes, "Failed to read carpet nodes", "Failed to read carpet node items", [this](const wxString &message) { return setErrorFromDatabase(message); } )) { FinalizeStatements({ nodeStmt, itemStmt }); return false; } FinalizeStatements({ nodeStmt, itemStmt }); return true; } bool BrushDatabaseBrushRepository::replaceTableNodes(int64_t brushId, const std::vector &nodes) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM table_nodes WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear table nodes"); } sqlite3_stmt* insertNodeStmt = nullptr; if (!prepare("INSERT INTO table_nodes(brush_id, align, sort_order) VALUES (?, ?, ?);", &insertNodeStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertItemStmt = nullptr; if (!prepare("INSERT INTO table_node_items(table_node_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertItemStmt)) { sqlite3_finalize(insertNodeStmt); rollbackTransaction(); return false; } if (!WriteAlignedNodesWithItems( connection_, brushId, nodes, { insertNodeStmt, insertItemStmt, "Failed to insert table node", "Failed to insert table node item" }, [this](const wxString &message) { return setErrorFromDatabase(message); } )) { FinalizeStatements({ insertNodeStmt, insertItemStmt }); rollbackTransaction(); return false; } FinalizeStatements({ insertNodeStmt, insertItemStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getTableNodes(int64_t brushId, std::vector &outNodes) { outNodes.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* nodeStmt = nullptr; if (!prepare("SELECT id, align, sort_order FROM table_nodes " "WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &nodeStmt)) { return false; } sqlite3_stmt* itemStmt = nullptr; if (!prepare("SELECT item_id, chance, sort_order FROM table_node_items " "WHERE table_node_id = ? ORDER BY sort_order ASC, id ASC;", &itemStmt)) { sqlite3_finalize(nodeStmt); return false; } sqlite3_bind_int64(nodeStmt, 1, brushId); if (!ReadAlignedNodesWithItems( nodeStmt, itemStmt, outNodes, "Failed to read table nodes", "Failed to read table node items", [this](const wxString &message) { return setErrorFromDatabase(message); } )) { FinalizeStatements({ nodeStmt, itemStmt }); return false; } FinalizeStatements({ nodeStmt, itemStmt }); return true; } bool BrushDatabaseBrushRepository::replaceDoodadAlternatives(int64_t brushId, const std::vector &alternatives) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } sqlite3_stmt* deleteStmt = nullptr; if (!prepare("DELETE FROM doodad_alternatives WHERE brush_id = ?;", &deleteStmt)) { rollbackTransaction(); return false; } sqlite3_bind_int64(deleteStmt, 1, brushId); int rc = sqlite3_step(deleteStmt); sqlite3_finalize(deleteStmt); if (rc != SQLITE_DONE) { rollbackTransaction(); return setErrorFromDatabase("Failed to clear doodad alternatives"); } sqlite3_stmt* insertAltStmt = nullptr; if (!prepare("INSERT INTO doodad_alternatives(brush_id, sort_order) VALUES (?, ?);", &insertAltStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertSingleStmt = nullptr; if (!prepare("INSERT INTO doodad_single_items(doodad_alternative_id, item_id, chance, sort_order) VALUES (?, ?, ?, ?);", &insertSingleStmt)) { sqlite3_finalize(insertAltStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertCompositeStmt = nullptr; if (!prepare("INSERT INTO doodad_composites(doodad_alternative_id, chance, sort_order) VALUES (?, ?, ?);", &insertCompositeStmt)) { sqlite3_finalize(insertAltStmt); sqlite3_finalize(insertSingleStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertTileStmt = nullptr; if (!prepare("INSERT INTO doodad_composite_tiles(doodad_composite_id, offset_x, offset_y, offset_z, sort_order) VALUES (?, ?, ?, ?, ?);", &insertTileStmt)) { sqlite3_finalize(insertAltStmt); sqlite3_finalize(insertSingleStmt); sqlite3_finalize(insertCompositeStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertTileItemStmt = nullptr; if (!prepare("INSERT INTO doodad_composite_tile_items(doodad_composite_tile_id, item_id, sort_order) VALUES (?, ?, ?);", &insertTileItemStmt)) { sqlite3_finalize(insertAltStmt); sqlite3_finalize(insertSingleStmt); sqlite3_finalize(insertCompositeStmt); sqlite3_finalize(insertTileStmt); rollbackTransaction(); return false; } const auto fail = [this, &insertAltStmt, &insertSingleStmt, &insertCompositeStmt, &insertTileStmt, &insertTileItemStmt](const wxString &message) { FinalizeStatements({ insertAltStmt, insertSingleStmt, insertCompositeStmt, insertTileStmt, insertTileItemStmt }); rollbackTransaction(); return setErrorFromDatabase(message); }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (const DoodadAlternativeRecord &alternative : alternatives) { sqlite3_reset(insertAltStmt); sqlite3_clear_bindings(insertAltStmt); sqlite3_bind_int64(insertAltStmt, 1, brushId); sqlite3_bind_int(insertAltStmt, 2, alternative.sortOrder); if (sqlite3_step(insertAltStmt) != SQLITE_DONE) { return fail("Failed to insert doodad alternative"); } const int64_t alternativeId = sqlite3_last_insert_rowid(connection_); if (!WriteDoodadSingleItems(alternativeId, alternative.singleItems, insertSingleStmt, setDbError)) { return fail(lastError_); } if (!WriteDoodadComposites( connection_, alternativeId, alternative.composites, insertCompositeStmt, insertTileStmt, insertTileItemStmt, setDbError )) { return fail(lastError_); } } FinalizeStatements({ insertAltStmt, insertSingleStmt, insertCompositeStmt, insertTileStmt, insertTileItemStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseBrushRepository::getDoodadAlternatives(int64_t brushId, std::vector &outAlternatives) { outAlternatives.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* altStmt = nullptr; if (!prepare("SELECT id, sort_order FROM doodad_alternatives " "WHERE brush_id = ? ORDER BY sort_order ASC, id ASC;", &altStmt)) { return false; } sqlite3_stmt* singleStmt = nullptr; if (!prepare("SELECT item_id, chance, sort_order FROM doodad_single_items " "WHERE doodad_alternative_id = ? ORDER BY sort_order ASC, id ASC;", &singleStmt)) { sqlite3_finalize(altStmt); return false; } sqlite3_stmt* compositeStmt = nullptr; if (!prepare("SELECT id, chance, sort_order FROM doodad_composites " "WHERE doodad_alternative_id = ? ORDER BY sort_order ASC, id ASC;", &compositeStmt)) { sqlite3_finalize(altStmt); sqlite3_finalize(singleStmt); return false; } sqlite3_stmt* tileStmt = nullptr; if (!prepare("SELECT id, offset_x, offset_y, offset_z, sort_order FROM doodad_composite_tiles " "WHERE doodad_composite_id = ? ORDER BY sort_order ASC, id ASC;", &tileStmt)) { sqlite3_finalize(altStmt); sqlite3_finalize(singleStmt); sqlite3_finalize(compositeStmt); return false; } sqlite3_stmt* tileItemStmt = nullptr; if (!prepare("SELECT item_id, sort_order FROM doodad_composite_tile_items " "WHERE doodad_composite_tile_id = ? ORDER BY sort_order ASC, id ASC;", &tileItemStmt)) { sqlite3_finalize(altStmt); sqlite3_finalize(singleStmt); sqlite3_finalize(compositeStmt); sqlite3_finalize(tileStmt); return false; } sqlite3_bind_int64(altStmt, 1, brushId); const auto fail = [this, &altStmt, &singleStmt, &compositeStmt, &tileStmt, &tileItemStmt](const wxString &message) { FinalizeStatements({ altStmt, singleStmt, compositeStmt, tileStmt, tileItemStmt }); return setErrorFromDatabase(message); }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (;;) { const int altRc = sqlite3_step(altStmt); if (altRc == SQLITE_DONE) { break; } if (altRc != SQLITE_ROW) { return fail("Failed to read doodad alternatives"); } const int64_t alternativeId = sqlite3_column_int64(altStmt, 0); DoodadAlternativeRecord alternative; alternative.sortOrder = sqlite3_column_int(altStmt, 1); if (!ReadDoodadSingleItems(alternativeId, singleStmt, alternative.singleItems, setDbError)) { return fail(lastError_); } if (!ReadDoodadComposites( alternativeId, compositeStmt, tileStmt, tileItemStmt, alternative.composites, setDbError )) { return fail(lastError_); } outAlternatives.push_back(alternative); } FinalizeStatements({ altStmt, singleStmt, compositeStmt, tileStmt, tileItemStmt }); return true; } bool BrushDatabaseCatalogRepository::replaceAllTilesets(const std::vector &tilesets) { if (!isOpen()) { return setError("SQLite database is not open."); } if (!beginTransaction()) { return false; } if (!execute("DELETE FROM tilesets;")) { rollbackTransaction(); return false; } sqlite3_stmt* insertTilesetStmt = nullptr; if (!prepare("INSERT INTO tilesets(name, source_file) VALUES (?, ?);", &insertTilesetStmt)) { rollbackTransaction(); return false; } sqlite3_stmt* insertSectionStmt = nullptr; if (!prepare("INSERT INTO tileset_sections(tileset_id, section_type, sort_order) VALUES (?, ?, ?);", &insertSectionStmt)) { sqlite3_finalize(insertTilesetStmt); rollbackTransaction(); return false; } sqlite3_stmt* insertEntryStmt = nullptr; if (!prepare("INSERT INTO tileset_brush_entries(tileset_section_id, entry_kind, brush_id, brush_name, item_id, from_item_id, to_item_id, after_brush_name, after_item_id, sort_order) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", &insertEntryStmt)) { sqlite3_finalize(insertTilesetStmt); sqlite3_finalize(insertSectionStmt); rollbackTransaction(); return false; } sqlite3_stmt* findBrushStmt = nullptr; if (!prepare("SELECT id FROM brushes WHERE name = ? LIMIT 2;", &findBrushStmt)) { sqlite3_finalize(insertTilesetStmt); sqlite3_finalize(insertSectionStmt); sqlite3_finalize(insertEntryStmt); rollbackTransaction(); return false; } const auto fail = [this, &insertTilesetStmt, &insertSectionStmt, &insertEntryStmt, &findBrushStmt](const wxString &message) { FinalizeStatements({ insertTilesetStmt, insertSectionStmt, insertEntryStmt, findBrushStmt }); rollbackTransaction(); return setErrorFromDatabase(message); }; const auto setDbError = [this](const wxString &message) { return setErrorFromDatabase(message); }; for (const TilesetStorageRecord &tileset : tilesets) { sqlite3_reset(insertTilesetStmt); sqlite3_clear_bindings(insertTilesetStmt); sqlite3_bind_text(insertTilesetStmt, 1, tileset.name.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(insertTilesetStmt, 2, tileset.sourceFile.utf8_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(insertTilesetStmt) != SQLITE_DONE) { return fail("Failed to insert tileset"); } const int64_t tilesetId = sqlite3_last_insert_rowid(connection_); for (const TilesetSectionRecord §ion : tileset.sections) { sqlite3_reset(insertSectionStmt); sqlite3_clear_bindings(insertSectionStmt); sqlite3_bind_int64(insertSectionStmt, 1, tilesetId); sqlite3_bind_text(insertSectionStmt, 2, section.sectionType.utf8_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(insertSectionStmt, 3, section.sortOrder); if (sqlite3_step(insertSectionStmt) != SQLITE_DONE) { return fail("Failed to insert tileset section"); } const int64_t sectionId = sqlite3_last_insert_rowid(connection_); if (!WriteTilesetEntries(sectionId, section.entries, insertEntryStmt, findBrushStmt, setDbError)) { return fail(lastError_); } } } FinalizeStatements({ insertTilesetStmt, insertSectionStmt, insertEntryStmt, findBrushStmt }); if (!commitTransaction()) { rollbackTransaction(); return false; } return true; } bool BrushDatabaseCatalogRepository::getTilesetByName(const wxString &name, TilesetStorageRecord &outTileset) { outTileset = TilesetStorageRecord(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* tilesetStmt = nullptr; if (!prepare("SELECT id, name, source_file FROM tilesets WHERE name = ? LIMIT 1;", &tilesetStmt)) { return false; } sqlite3_bind_text(tilesetStmt, 1, name.utf8_str(), -1, SQLITE_TRANSIENT); const int rc = sqlite3_step(tilesetStmt); if (rc == SQLITE_DONE) { sqlite3_finalize(tilesetStmt); return setError("Tileset '" + name + "' was not found in SQLite."); } if (rc != SQLITE_ROW) { sqlite3_finalize(tilesetStmt); return setErrorFromDatabase("Failed to query tileset by name"); } const int64_t tilesetId = sqlite3_column_int64(tilesetStmt, 0); outTileset.name = ToWxString(reinterpret_cast(sqlite3_column_text(tilesetStmt, 1))); outTileset.sourceFile = ToWxString(reinterpret_cast(sqlite3_column_text(tilesetStmt, 2))); sqlite3_finalize(tilesetStmt); sqlite3_stmt* sectionStmt = nullptr; if (!prepare("SELECT id, section_type, sort_order FROM tileset_sections " "WHERE tileset_id = ? ORDER BY sort_order ASC, id ASC;", §ionStmt)) { return false; } sqlite3_stmt* entryStmt = nullptr; if (!prepare("SELECT entry_kind, brush_id, brush_name, item_id, from_item_id, to_item_id, after_brush_name, after_item_id, sort_order " "FROM tileset_brush_entries WHERE tileset_section_id = ? ORDER BY sort_order ASC, id ASC;", &entryStmt)) { sqlite3_finalize(sectionStmt); return false; } sqlite3_bind_int64(sectionStmt, 1, tilesetId); for (;;) { const int sectionRc = sqlite3_step(sectionStmt); if (sectionRc == SQLITE_DONE) { break; } if (sectionRc != SQLITE_ROW) { sqlite3_finalize(sectionStmt); sqlite3_finalize(entryStmt); return setErrorFromDatabase("Failed to read tileset sections"); } const int64_t sectionId = sqlite3_column_int64(sectionStmt, 0); TilesetSectionRecord section; section.sectionType = ToWxString(reinterpret_cast(sqlite3_column_text(sectionStmt, 1))); section.sortOrder = sqlite3_column_int(sectionStmt, 2); sqlite3_reset(entryStmt); sqlite3_clear_bindings(entryStmt); sqlite3_bind_int64(entryStmt, 1, sectionId); for (;;) { const int entryRc = sqlite3_step(entryStmt); if (entryRc == SQLITE_DONE) { break; } if (entryRc != SQLITE_ROW) { sqlite3_finalize(sectionStmt); sqlite3_finalize(entryStmt); return setErrorFromDatabase("Failed to read tileset entries"); } TilesetEntryRecord entry; entry.entryKind = ToWxString(reinterpret_cast(sqlite3_column_text(entryStmt, 0))); entry.brushId = ReadNullableInt64(entryStmt, 1); entry.brushName = ToWxString(reinterpret_cast(sqlite3_column_text(entryStmt, 2))); entry.itemId = sqlite3_column_int(entryStmt, 3); entry.fromItemId = sqlite3_column_int(entryStmt, 4); entry.toItemId = sqlite3_column_int(entryStmt, 5); entry.afterBrushName = ToWxString(reinterpret_cast(sqlite3_column_text(entryStmt, 6))); entry.afterItemId = sqlite3_column_int(entryStmt, 7); entry.sortOrder = sqlite3_column_int(entryStmt, 8); section.entries.push_back(entry); } outTileset.sections.push_back(section); } sqlite3_finalize(sectionStmt); sqlite3_finalize(entryStmt); return true; } bool BrushDatabaseCatalogRepository::getAllTilesets(std::vector &outTilesets) { outTilesets.clear(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* stmt = nullptr; if (!prepare("SELECT name FROM tilesets ORDER BY name ASC, id ASC;", &stmt)) { return false; } for (;;) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { break; } if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return setErrorFromDatabase("Failed to list tilesets"); } TilesetStorageRecord tileset; if (!getTilesetByName(ToWxString(reinterpret_cast(sqlite3_column_text(stmt, 0))), tileset)) { sqlite3_finalize(stmt); return false; } outTilesets.push_back(tileset); } sqlite3_finalize(stmt); return true; } bool BrushDatabaseCatalogRepository::generateAuditReport(MaterialsDatabaseAuditReport &outReport) { outReport = MaterialsDatabaseAuditReport(); if (!isOpen()) { return setError("SQLite database is not open."); } sqlite3_stmt* countStmt = nullptr; if (!prepare("SELECT " "(SELECT COUNT(*) FROM brushes), " "(SELECT COUNT(*) FROM border_sets), " "(SELECT COUNT(*) FROM tilesets), " "(SELECT COUNT(*) FROM tileset_sections), " "(SELECT COUNT(*) FROM tileset_brush_entries), " "(SELECT COUNT(*) FROM ground_brush_borders WHERE target_mode = 'brush' AND target_brush_name <> '' AND target_brush_id IS NULL), " "(SELECT COUNT(*) FROM brush_links WHERE target_brush_name <> '' AND target_brush_name <> 'all' AND target_brush_id IS NULL), " "(SELECT COUNT(*) FROM tileset_brush_entries WHERE brush_name <> '' AND brush_id IS NULL);", &countStmt)) { return false; } const int rc = sqlite3_step(countStmt); if (rc != SQLITE_ROW) { sqlite3_finalize(countStmt); return setErrorFromDatabase("Failed to build SQLite audit report"); } outReport.brushCount = sqlite3_column_int(countStmt, 0); outReport.borderSetCount = sqlite3_column_int(countStmt, 1); outReport.tilesetCount = sqlite3_column_int(countStmt, 2); outReport.tilesetSectionCount = sqlite3_column_int(countStmt, 3); outReport.tilesetEntryCount = sqlite3_column_int(countStmt, 4); outReport.unresolvedGroundTargets = sqlite3_column_int(countStmt, 5); outReport.unresolvedBrushLinks = sqlite3_column_int(countStmt, 6); outReport.unresolvedTilesetEntries = sqlite3_column_int(countStmt, 7); sqlite3_finalize(countStmt); sqlite3_stmt* groupedStmt = nullptr; if (!prepare("SELECT type, COUNT(*) FROM brushes GROUP BY type ORDER BY type ASC;", &groupedStmt)) { return false; } for (;;) { const int groupedRc = sqlite3_step(groupedStmt); if (groupedRc == SQLITE_DONE) { break; } if (groupedRc != SQLITE_ROW) { sqlite3_finalize(groupedStmt); return setErrorFromDatabase("Failed to group brushes by type"); } BrushTypeCountRecord typeCount; typeCount.type = ToWxString(reinterpret_cast(sqlite3_column_text(groupedStmt, 0))); typeCount.count = sqlite3_column_int(groupedStmt, 1); outReport.brushTypeCounts.push_back(typeCount); } sqlite3_finalize(groupedStmt); return true; } bool BrushDatabaseCatalogRepository::hasCompleteImportForCurrentSchema(bool &outReady) { outReady = false; if (!isOpen()) { return setError("SQLite database is not open."); } int version = 0; if (!schemaManager_.getCurrentSchemaVersion(version)) { return false; } if (version != kBrushDatabaseSchemaVersion) { return true; } MaterialsDatabaseAuditReport report; if (!generateAuditReport(report)) { return false; } bool hasGround = false; bool hasWall = false; bool hasDoodad = false; bool hasCarpet = false; bool hasTable = false; for (const BrushTypeCountRecord &typeCount : report.brushTypeCounts) { if (typeCount.type == "ground" && typeCount.count > 0) { hasGround = true; } else if (typeCount.type == "wall" && typeCount.count > 0) { hasWall = true; } else if (typeCount.type == "doodad" && typeCount.count > 0) { hasDoodad = true; } else if (typeCount.type == "carpet" && typeCount.count > 0) { hasCarpet = true; } else if (typeCount.type == "table" && typeCount.count > 0) { hasTable = true; } } outReady = hasGround && hasWall && hasDoodad && hasCarpet && hasTable && report.borderSetCount > 0 && report.tilesetCount > 0; return true; } int BrushDatabaseCatalogRepository::getExpectedSchemaVersion() const { return kBrushDatabaseSchemaVersion; } bool BrushDatabaseBrushRepository::resolveGroundReferenceNames() { if (!isOpen()) { return setError("SQLite database is not open."); } if (!execute("UPDATE ground_brush_borders " "SET target_brush_id = (" "SELECT id FROM brushes b " "WHERE b.name = ground_brush_borders.target_brush_name AND b.type = 'ground' " "LIMIT 1" ") " "WHERE target_brush_name <> '' AND target_mode = 'brush';")) { return false; } return execute("UPDATE brush_links " "SET target_brush_id = (" "SELECT id FROM brushes b " "WHERE b.name = brush_links.target_brush_name " "LIMIT 1" ") " "WHERE target_brush_name <> '' AND target_brush_name <> 'all';"); } bool BrushDatabaseSession::execute(const wxString &sql) { char* errorMessage = nullptr; const int rc = sqlite3_exec(connection_, sql.utf8_str(), nullptr, nullptr, &errorMessage); if (rc != SQLITE_OK) { const wxString detail = errorMessage ? wxString::FromUTF8(errorMessage) : wxString(); sqlite3_free(errorMessage); return setError("SQLite exec failed: " + detail); } return true; } bool BrushDatabaseSession::prepare(const char* sql, sqlite3_stmt** stmt) { const int rc = sqlite3_prepare_v2(connection_, sql, -1, stmt, nullptr); if (rc != SQLITE_OK) { return setErrorFromDatabase("Failed to prepare SQLite statement"); } return true; } bool BrushDatabaseSession::beginTransaction() { if (readOnly_) { return setError("SQLite transaction requires a read-write connection."); } if (transactionDepth_ == 0) { if (!execute("BEGIN IMMEDIATE TRANSACTION;")) { return false; } transactionDepth_ = 1; savepointIds_.clear(); return true; } const int savepointId = nextSavepointId_++; const wxString savepointName = MakeTransactionSavepointName(savepointId); if (!execute("SAVEPOINT " + savepointName + ";")) { return false; } savepointIds_.push_back(savepointId); ++transactionDepth_; return true; } bool BrushDatabaseSession::commitTransaction() { if (readOnly_) { return setError("SQLite transaction requires a read-write connection."); } if (transactionDepth_ <= 0) { return setError("SQLite commit requested without an active transaction."); } if (transactionDepth_ == 1) { if (!execute("COMMIT;")) { return false; } transactionDepth_ = 0; savepointIds_.clear(); return true; } const int savepointId = savepointIds_.back(); const wxString savepointName = MakeTransactionSavepointName(savepointId); if (!execute("RELEASE SAVEPOINT " + savepointName + ";")) { return false; } savepointIds_.pop_back(); --transactionDepth_; return true; } bool BrushDatabaseSession::rollbackTransaction() { if (readOnly_) { return setError("SQLite transaction requires a read-write connection."); } if (transactionDepth_ <= 0) { return setError("SQLite rollback requested without an active transaction."); } if (transactionDepth_ == 1) { if (!execute("ROLLBACK;")) { return false; } transactionDepth_ = 0; savepointIds_.clear(); return true; } const int savepointId = savepointIds_.back(); const wxString savepointName = MakeTransactionSavepointName(savepointId); if (!execute("ROLLBACK TO SAVEPOINT " + savepointName + ";")) { return false; } if (!execute("RELEASE SAVEPOINT " + savepointName + ";")) { return false; } savepointIds_.pop_back(); --transactionDepth_; return true; } bool BrushDatabaseSession::setError(const wxString &message) { lastError_ = message; spdlog::error("[BrushDatabase] {}", lastError_.ToStdString()); return false; } bool BrushDatabaseSession::setErrorFromDatabase(const wxString &prefix) { const wxString dbMessage = connection_ ? ToWxString(sqlite3_errmsg(connection_)) : wxString("No SQLite connection"); return setError(prefix + ": " + dbMessage); }