table = $wpdb->prefix . 'shiftora_backup_pins'; } /** * The CREATE TABLE statement for dbDelta, without IF NOT EXISTS because * dbDelta cannot diff a declaration that has it. */ public static function table_ddl( string $table, string $charset_collate ): string { return "CREATE TABLE `{$table}` ( backup_id BIGINT UNSIGNED NOT NULL, migration_id BIGINT UNSIGNED NOT NULL, pin_kind VARCHAR(16) NOT NULL DEFAULT 'recovery', created_at DATETIME NOT NULL, PRIMARY KEY (backup_id), KEY migration_id (migration_id) ) {$charset_collate}"; } /** Records or refreshes the pin a lock places on its restore point. */ public function pin( int $backup_id, int $migration_id ): void { $wpdb = $this->wpdb; $now = current_time( 'mysql', true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $result = $wpdb->query( $wpdb->prepare( "INSERT INTO %i (backup_id, migration_id, pin_kind, created_at) VALUES (%d, %d, 'recovery', %s) ON DUPLICATE KEY UPDATE migration_id = VALUES(migration_id), pin_kind = VALUES(pin_kind), created_at = VALUES(created_at)", $this->table, $backup_id, $migration_id, $now ) ); if ( false === $result ) { throw new RuntimeException( "Failed to pin backup {$backup_id}: {$this->wpdb->last_error}" ); } } /** Releases every pin a migration placed, when its lock is cleared. */ public function unpin_for_migration( int $migration_id ): void { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE migration_id = %d', $this->table, $migration_id ) ); if ( false === $result ) { throw new RuntimeException( "Failed to unpin migration {$migration_id}: {$this->wpdb->last_error}" ); } } /** * Backup ids retention may not delete. * * @return int[] */ public function pinned_backup_ids(): array { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT backup_id FROM %i', $this->table ) ); if ( ! is_array( $rows ) ) { return []; } $ids = array_map( static fn( object $r ): int => (int) $r->backup_id, $rows ); sort( $ids, SORT_NUMERIC ); return $ids; } /** * Migration ids that still hold a pin, for the unfinished-scan filter. * * @return int[] */ public function pinned_migration_ids(): array { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT DISTINCT migration_id FROM %i', $this->table ) ); if ( ! is_array( $rows ) ) { return []; } $ids = array_map( static fn( object $r ): int => (int) $r->migration_id, $rows ); sort( $ids, SORT_NUMERIC ); return $ids; } /** * Every current pin, grouped by migration. The durable half of a lock, so a * failed run is still reported after the authorizing transient is gone. Read * once per recovery pass, not per migration. * * @return array Migration id => backup ids, ascending. */ public function pins_by_migration(): array { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT migration_id, backup_id FROM %i', $this->table ) ); if ( ! is_array( $rows ) ) { return []; } $grouped = []; foreach ( $rows as $row ) { $grouped[ (int) $row->migration_id ][] = (int) $row->backup_id; } foreach ( $grouped as $migration_id => $backup_ids ) { sort( $backup_ids, SORT_NUMERIC ); $grouped[ $migration_id ] = $backup_ids; } ksort( $grouped, SORT_NUMERIC ); return $grouped; } // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped }