diff --git a/backend/src/main/java/com/storycove/service/DatabaseManagementService.java b/backend/src/main/java/com/storycove/service/DatabaseManagementService.java index 705f01d..7e84fe7 100644 --- a/backend/src/main/java/com/storycove/service/DatabaseManagementService.java +++ b/backend/src/main/java/com/storycove/service/DatabaseManagementService.java @@ -178,7 +178,10 @@ public class DatabaseManagementService { /** * Restore from complete backup (ZIP format) */ - @Transactional(timeout = 1800) // 30 minutes timeout for large backup restores + // Not @Transactional: this method launches psql as an external process. If a Spring + // transaction were active here, the ROW EXCLUSIVE locks held by clearAllDataAndFiles() + // would block psql's DROP TABLE IF EXISTS (ACCESS EXCLUSIVE), causing a deadlock. + // Each inner method (clearAllDataAndFiles, restoreFromBackup) manages its own transaction. public void restoreFromCompleteBackup(InputStream backupStream) throws IOException, SQLException { String currentLibraryId = libraryService.getCurrentLibraryId(); System.err.println("Starting complete backup restore for library: " + currentLibraryId); @@ -498,92 +501,6 @@ public class DatabaseManagementService { return totalDeleted; } - /** - * Parses SQL content into individual statements, properly handling semicolons inside string literals - */ - private List parseStatements(String sql) { - List statements = new ArrayList<>(); - StringBuilder currentStatement = new StringBuilder(); - boolean inString = false; - - for (int i = 0; i < sql.length(); i++) { - char c = sql.charAt(i); - - if (c == '\'' && !inString) { - // Start of string literal - inString = true; - currentStatement.append(c); - } else if (c == '\'' && inString) { - // Potential end of string literal - currentStatement.append(c); - - // Check if this is an escaped quote (doubled single quote) - if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { - // This is an escaped quote, skip the next quote - i++; - currentStatement.append('\''); - } else { - // End of string literal - inString = false; - } - } else if (c == ';' && !inString) { - // Statement terminator outside of string literal - String statement = currentStatement.toString().trim(); - if (!statement.isEmpty()) { - statements.add(statement); - } - currentStatement = new StringBuilder(); - } else { - currentStatement.append(c); - } - } - - // Add final statement if any - String finalStatement = currentStatement.toString().trim(); - if (!finalStatement.isEmpty()) { - statements.add(finalStatement); - } - - return statements; - } - - /** - * Formats a database value for SQL insertion, handling proper escaping - */ - private String formatSqlValue(Object value) { - if (value == null) { - return "NULL"; - } - - if (value instanceof Boolean) { - return ((Boolean) value) ? "true" : "false"; - } - - if (value instanceof Number) { - return value.toString(); - } - - // Handle all other types as strings (String, UUID, Timestamp, CLOB, TEXT, etc.) - String stringValue; - - // Special handling for CLOB types - if (value instanceof Clob) { - Clob clob = (Clob) value; - try { - stringValue = clob.getSubString(1, (int) clob.length()); - } catch (SQLException e) { - stringValue = value.toString(); - } - } else { - stringValue = value.toString(); - } - - // Escape single quotes by replacing ' with '' and wrap in quotes - String escapedValue = stringValue.replace("'", "''"); - - return "'" + escapedValue + "'"; - } - /** * Clear all data AND files (for complete restore) */ @@ -654,219 +571,6 @@ public class DatabaseManagementService { } } - /** - * Ensure database schema exists before restoring backup data. - * This creates all necessary tables, indexes, and constraints if they don't exist. - */ - private void ensureDatabaseSchemaExists(Connection connection) throws SQLException { - try { - // Check if a key table exists to determine if schema is already created - String checkTableQuery = "SELECT 1 FROM information_schema.tables WHERE table_name = 'stories' LIMIT 1"; - try (PreparedStatement stmt = connection.prepareStatement(checkTableQuery); - var resultSet = stmt.executeQuery()) { - if (resultSet.next()) { - System.err.println("Database schema already exists, skipping schema creation."); - return; // Schema exists - } - } - - System.err.println("Creating database schema for restore in library: " + libraryService.getCurrentLibraryId()); - - // Create the schema using the same DDL as LibraryService - String[] createTableStatements = { - // Authors table - """ - CREATE TABLE authors ( - author_rating integer, - created_at timestamp(6) not null, - updated_at timestamp(6) not null, - id uuid not null, - avatar_image_path varchar(255), - name varchar(255) not null, - notes TEXT, - primary key (id) - ) - """, - - // Author URLs table - """ - CREATE TABLE author_urls ( - author_id uuid not null, - url varchar(255) - ) - """, - - // Series table - """ - CREATE TABLE series ( - created_at timestamp(6) not null, - id uuid not null, - description varchar(1000), - name varchar(255) not null, - primary key (id) - ) - """, - - // Tags table - """ - CREATE TABLE tags ( - color varchar(7), - created_at timestamp(6) not null, - id uuid not null, - description varchar(500), - name varchar(255) not null unique, - primary key (id) - ) - """, - - // Tag aliases table - """ - CREATE TABLE tag_aliases ( - created_from_merge boolean not null, - created_at timestamp(6) not null, - canonical_tag_id uuid not null, - id uuid not null, - alias_name varchar(255) not null unique, - primary key (id) - ) - """, - - // Collections table - """ - CREATE TABLE collections ( - is_archived boolean not null, - rating integer, - created_at timestamp(6) not null, - updated_at timestamp(6) not null, - id uuid not null, - cover_image_path varchar(500), - name varchar(500) not null, - description TEXT, - primary key (id) - ) - """, - - // Stories table - """ - CREATE TABLE stories ( - is_read boolean, - rating integer, - reading_position integer, - volume integer, - word_count integer, - created_at timestamp(6) not null, - last_read_at timestamp(6), - updated_at timestamp(6) not null, - author_id uuid, - id uuid not null, - series_id uuid, - description varchar(1000), - content_html TEXT, - content_plain TEXT, - cover_path varchar(255), - source_url varchar(255), - summary TEXT, - title varchar(255) not null, - primary key (id) - ) - """, - - // Reading positions table - """ - CREATE TABLE reading_positions ( - chapter_index integer, - character_position integer, - percentage_complete float(53), - word_position integer, - created_at timestamp(6) not null, - updated_at timestamp(6) not null, - id uuid not null, - story_id uuid not null, - context_after varchar(500), - context_before varchar(500), - chapter_title varchar(255), - epub_cfi TEXT, - primary key (id) - ) - """, - - // Junction tables - """ - CREATE TABLE story_tags ( - story_id uuid not null, - tag_id uuid not null, - primary key (story_id, tag_id) - ) - """, - - """ - CREATE TABLE collection_stories ( - position integer not null, - added_at timestamp(6) not null, - collection_id uuid not null, - story_id uuid not null, - primary key (collection_id, story_id), - unique (collection_id, position) - ) - """, - - """ - CREATE TABLE collection_tags ( - collection_id uuid not null, - tag_id uuid not null, - primary key (collection_id, tag_id) - ) - """ - }; - - String[] createIndexStatements = { - "CREATE INDEX idx_reading_position_story ON reading_positions (story_id)" - }; - - String[] createConstraintStatements = { - // Foreign key constraints - "ALTER TABLE author_urls ADD CONSTRAINT FKdqhp51m0uveybsts098gd79uo FOREIGN KEY (author_id) REFERENCES authors", - "ALTER TABLE stories ADD CONSTRAINT FKhwecpqeaxy40ftrctef1u7gw7 FOREIGN KEY (author_id) REFERENCES authors", - "ALTER TABLE stories ADD CONSTRAINT FK1kulyvy7wwcolp2gkndt57cp7 FOREIGN KEY (series_id) REFERENCES series", - "ALTER TABLE reading_positions ADD CONSTRAINT FKglfhdhflan3pgyr2u0gxi21i5 FOREIGN KEY (story_id) REFERENCES stories", - "ALTER TABLE story_tags ADD CONSTRAINT FKmans33ijt0nf65t0sng2r848j FOREIGN KEY (tag_id) REFERENCES tags", - "ALTER TABLE story_tags ADD CONSTRAINT FKq9guid7swnjxwdpgxj3jo1rsi FOREIGN KEY (story_id) REFERENCES stories", - "ALTER TABLE tag_aliases ADD CONSTRAINT FKqfsawmcj3ey4yycb6958y24ch FOREIGN KEY (canonical_tag_id) REFERENCES tags", - "ALTER TABLE collection_stories ADD CONSTRAINT FKr55ho4vhj0wp03x13iskr1jds FOREIGN KEY (collection_id) REFERENCES collections", - "ALTER TABLE collection_stories ADD CONSTRAINT FK7n41tbbrt7r2e81hpu3612r1o FOREIGN KEY (story_id) REFERENCES stories", - "ALTER TABLE collection_tags ADD CONSTRAINT FKceq7ggev8n8ibjui1x5yo4x67 FOREIGN KEY (tag_id) REFERENCES tags", - "ALTER TABLE collection_tags ADD CONSTRAINT FKq9sa5s8csdpbphrvb48tts8jt FOREIGN KEY (collection_id) REFERENCES collections" - }; - - // Create tables - for (String sql : createTableStatements) { - try (var statement = connection.createStatement()) { - statement.executeUpdate(sql); - } - } - - // Create indexes - for (String sql : createIndexStatements) { - try (var statement = connection.createStatement()) { - statement.executeUpdate(sql); - } - } - - // Create constraints - for (String sql : createConstraintStatements) { - try (var statement = connection.createStatement()) { - statement.executeUpdate(sql); - } - } - - System.err.println("Database schema created successfully for restore."); - - } catch (SQLException e) { - System.err.println("Error creating database schema: " + e.getMessage()); - throw e; - } - } - /** * Add database dump to ZIP archive */