OJS Upgrade Service: Version-by-Version Migration & DOI Fix

The Challenge

Client: UDekom Balkans
Website: https://balkans-jetss.org
Problem: Scientific journal on OJS 3.3.0.9 with non-functional DOI assignment

Udekom Balkans contacted us in need of assistance. Their academic journal’s DOI (Digital Object Identifier) assignment system had stopped working—a critical issue for any scientific publication. Citations, indexing, and academic credibility all depend on functional DOIs.

After researching online forums, they found the common recommendation: “Just upgrade to the latest OJS version.” They reached out to us, hoping for a straightforward solution.

What we discovered would prove this advice dangerously wrong.

NOTICE: We do not claim that this is entirely fault of the OJS and PKP, this is the interpretation of our experience that may be highly subjective. The purpose of this post is to help someone who may experience similar problems and to point out some possible areas of improvements for the OJS.

Attempt #1: The “Direct Upgrade from OJS 3.3.x to 3.5.x” Disaster

Trying OJS 3.3.0.9 → 3.5.0 (The “Happy Path”)

Following PKP’s official documentation, we attempted a direct upgrade to OJS 3.5.0, the latest stable release at the time.

Result: Complete failure.

Technical Problems Encountered:

1. Database Schema Mismatch


SQLSTATE[42S22]: Column not found: 1054 Unknown column 'at.assoc_type' in 'WHERE'

The upgrade script assumed database columns that didn’t exist in the 3.3.0.9 schema.

2. Missing Configuration Values

[email]
; Missing: default = sendmail
; Missing: smtp_server configuration
; Missing: encryption key in [security] section

Critical config.inc.php values were missing, causing the upgrade to fail silently.

3. Circular Table Dependencies


ERROR: Cannot rename 'notification_subscription_settings.context_id' 
to 'context' - column 'context' already exists

The migration script tried to:

  • Create column context
  • Copy data from context_id to context
  • Drop context_id
  • Rename context to context_id

This created an infinite loop, with the script fighting against itself.

4. No Rollback Mechanism

When migrations failed halfway through:

❌ Tables left in inconsistent state

❌ Some migrations applied, others not

❌ No automatic rollback

❌ Manual database restoration required

5. Index Name Conflicts


SQLSTATE[42000]: Duplicate key name 'publications_doi_id'

Upgrade scripts attempted to create indexes that already existed from previous failed attempts.

6. Foreign Key Constraint Violations


ERROR: Cannot add foreign key constraint 
'notification_subscription_settings_context_foreign'
```
Data integrity issues prevented foreign key creation.

**Decision:** Abort and restore from backup. Direct upgrade to 3.5.0 is **impossible** from 3.3.0.9.

---

## The ONLY Solution: Version-by-Version Migration

After analyzing the codebase and migration scripts, we realized **there is no shortcut with OJS upgrades.**

### Our Methodology

We performed **incremental upgrades** through every single minor version:

**Upgrade Path:**
```
3.3.0.9 → 3.3.0.10 → 3.3.0.11 → 3.3.0.12 → 3.3.0.13 → 3.3.0.14 
→ 3.3.0.15 → 3.3.0.16 → 3.4.0.0 → 3.4.0.1 → 3.4.0.2 → ... 
→ 3.4.0.10 ✓

Attempt #2: The “OJS Incremental Upgrade”

Process for Each Version:

1. Pre-Upgrade Backup


# Database snapshot
mysqldump -h "hostname" -u "username" -p \\
  db_name| gzip > \\
  backup_$(date +%Y%m%d_%H%M).sql.gz

# File system backup  
tar -czf public_html_backup_$(date +%Y%m%d_%H%M).tar.gz public_html/

2. Manual Database Fixes

At several versions, we had to manually create missing structures:


-- Example: Missing 'redirect' column in 3.4.0.2
ALTER TABLE site ADD COLUMN IF NOT EXISTS redirect BIGINT DEFAULT NULL;

-- Example: Missing 'assoc_type' in announcement_types
ALTER TABLE announcement_types 
  ADD COLUMN IF NOT EXISTS assoc_type BIGINT(20) DEFAULT NULL AFTER context_id,
  ADD COLUMN IF NOT EXISTS assoc_id BIGINT(20) DEFAULT NULL AFTER assoc_type;

-- Example: Fixing email_templates_default_data structure
ALTER TABLE email_templates_default_data 
  ADD COLUMN IF NOT EXISTS name TEXT DEFAULT NULL AFTER locale;

3. Config File Updates

Critical config.inc.php values were missing, causing the upgrade to fail silently.


# Add missing email configuration
echo "default = sendmail" >> config.inc.php

# Verify encryption key exists
grep "encryption =" config.inc.php || \\
  echo "encryption = [generated_key]" >> config.inc.php

4. Upgrade Execution


php tools/upgrade.php upgrade

5. Post-Upgrade Validation

✓ Test user login

✓ Test article submission workflow

✓ Verify file uploads

✓ Check database integrity

✓ Review error logs

6. Cache Clearing


rm -rf cache/*

Result: Successfully reached OJS 3.4.0.10!

Why Migration from OJS 3.4 to OJS 3.5 is Currently Impossible

Critical Bugs in 3.4.x → 3.5.0 Migration

We attempted the final jump to 3.5.0 after stabilizing on 3.4.0.10.

Blockers Encountered:

Error #1: Circular Column Rename Loop


SQLSTATE[42S21]: Column 'site.redirect_context_id' already exists

Migration script logic:
1. ALTER TABLE site ADD COLUMN redirect
2. UPDATE site SET redirect = redirect_context_id  
3. ALTER TABLE site DROP COLUMN redirect_context_id
4. ALTER TABLE site CHANGE redirect redirect_context_id

Problem: Step 4 fails because redirect_context_id still exists

Error #2: Notification Settings Table Deadlock


SQLSTATE[42S22]: Column 'context' not found
SQLSTATE[42S01]: Column 'context' already exists

The script simultaneously tries to:
- Create 'context' column
- Drop 'context_id' column  
- Rename 'context' to 'context_id'

This creates an unresolvable state.

Error #3: Email Template Migration Failure


SQLSTATE[42000]: Syntax error in SQL statement  
Attempted to use double quotes in VALUES clause
```

**Conclusion:** OJS 3.5.0 migration from 3.4.x has **unresolved bugs** as of February 2026. We recommend staying on 3.4.0.10 until PKP releases patches.

---

## Problem #2: DOI Management Still Broken

After successfully reaching 3.4.0.10, we expected DOI functionality to work.

**It didn't.**

### Error #1: "An unexpected error has occurred"

When clicking on any article in the DOI management page:
```
[Browser displays]
An unexpected error has occurred. Please reload the page and try again.

[PHP error log shows]
SQLSTATE[42S22]: Column not found: 1054 
Unknown column 'current_p.doi_id' in 'WHERE'
```

**Root Cause:** The `publications` table was missing the `doi_id` column entirely.

### Error #2: "Some DOI(s) could not be updated"

When attempting to assign DOIs:
```
[UI shows generic error]
Some DOI(s) could not be updated

[Network tab reveals actual errors]
{"doi": ["This is not formatted correctly."]}
{"doi": ["The given DOI suffix is already in use for another published item."]}

Root Cause: Multiple database structural issues.

Fixing DOI problems on OJS 3.4.X

After setting up 3.4.X version problems with DOI still persisted.

4 major problems occured:

1. Opening DOI screen showed error mesage: “An unexpected error has occured. Please reload the page and try again.”

  1. 2. Attempting to edit DOI for a single publication over DOI field showed error message: “Some DOI(s) could not be updated”

  1. 3. Previously entered publication DOI vanished
  2. 4. PDF for publications were missing.

Solution FOR DOI problems on OJS 3.4.x

To solve these problems following path was taken:

OJS 3.4 introduced a completely new DOI architecture:

Old system (OJS 3.3):

  • DOIs stored in publication_settings table
  • Setting name: pub-id::doi
  • No dedicated DOI table

New system (OJS 3.4):

  • Dedicated dois table
  • Dedicated doi_settings table
  • publications.doi_id foreign key column
  • Link table structure

The upgrade script failed to:

  1. Create the dois table
  2. Create the doi_settings table
  3. Add doi_id column to publications
  4. Migrate legacy DOIs to new structure

Missing PDF happened because migration manuals on the web did not mention to copy the uploaded files folder, they were later copied from the backup.

The Fix: Complete SQL Migration

Step 1: Create Missing Tables


S-- Create dois table
CREATE TABLE IF NOT EXISTS dois (
    doi_id BIGINT NOT NULL AUTO_INCREMENT,
    context_id BIGINT NOT NULL,
    doi VARCHAR(255) NOT NULL,
    status SMALLINT DEFAULT 1,
    PRIMARY KEY (doi_id),
    UNIQUE KEY dois_unique (doi),
    KEY dois_context_id (context_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- Create doi_settings table
CREATE TABLE IF NOT EXISTS doi_settings (
    doi_setting_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    doi_id BIGINT NOT NULL,
    locale VARCHAR(28) NOT NULL DEFAULT '',
    setting_name VARCHAR(255) NOT NULL,
    setting_value MEDIUMTEXT,
    PRIMARY KEY (doi_setting_id),
    UNIQUE KEY doi_settings_unique (doi_id, locale, setting_name),
    KEY doi_settings_doi_id (doi_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

Step 2: Add Missing Column to Publications


-- Add doi_id foreign key column
ALTER TABLE publications 
ADD COLUMN doi_id BIGINT(20) NULL;

-- Add index for performance
ALTER TABLE publications 
ADD INDEX publications_doi_id (doi_id);

Step 3: Migrate Legacy DOI Data


-- Migrate DOIs from publication_settings to dois table
INSERT INTO dois (context_id, doi, status)
SELECT 
    1 as context_id,
    ps.setting_value as doi,
    1 as status
FROM publication_settings ps
WHERE ps.setting_name = 'pub-id::doi'
AND ps.setting_value IS NOT NULL
AND ps.setting_value != ''
AND NOT EXISTS (
    SELECT 1 FROM dois d 
    WHERE d.doi = ps.setting_value
)
GROUP BY ps.setting_value;

-- Result: 113 rows inserted

Step 4: Link DOIs to Publications


-- Create publication_id links in doi_settings
INSERT INTO doi_settings (doi_id, locale, setting_name, setting_value)
SELECT 
    d.doi_id,
    '' as locale,
    'publication_id' as setting_name,
    ps.publication_id as setting_value
FROM dois d
INNER JOIN publication_settings ps ON ps.setting_value = d.doi
WHERE ps.setting_name = 'pub-id::doi'
AND NOT EXISTS (
    SELECT 1 FROM doi_settings ds 
    WHERE ds.doi_id = d.doi_id 
    AND ds.setting_name = 'publication_id'
);

-- Result: 116 rows inserted

Step 5: Add Context Path


-- Add journal context path for each DOI
INSERT INTO doi_settings (doi_id, locale, setting_name, setting_value)
SELECT 
    d.doi_id,
    '' as locale,
    'contextPath' as setting_name,
    'index_bjetss' as setting_value
FROM dois d
WHERE NOT EXISTS (
    SELECT 1 FROM doi_settings ds 
    WHERE ds.doi_id = d.doi_id 
    AND ds.setting_name = 'contextPath'
);

-- Result: 114 rows inserted

Step 6: Populate publications.doi_id


-- Link DOIs back to publications table
UPDATE publications p
INNER JOIN doi_settings ds ON ds.setting_value = p.publication_id 
  AND ds.setting_name = 'publication_id'
SET p.doi_id = ds.doi_id;

-- Result: 113 publications updated

Step 7: Clear Application Cache


-rm -rf cache/*
```

**Result:** DOI management page now loads successfully, showing all 113 migrated DOIs.

---

## The Frontend UX Disaster

Even after fixing the database, the **user experience was terrible**.

### Problem #1: Cryptic Error Messages

**What users see:**
```
Some DOI(s) could not be updated

We also like to point out that frontend error messages are wrong. They are correctly implemented on the endpoint but the end user does not understand what is the real problem because endpoint response was never shown to the user. Experienced users may use console to debug the problem. This is not fixed in the newest version 3.5 demo on PKP website.

What’s actually happening:

Scenario A: Invalid format


// User enters: https://doi.org/10.31410/Balkans.JETSS.2025.8.2.123-137
// API returns:
{
  "doi": ["This is not formatted correctly."]
}

What the error SHOULD say:

“Please enter only the DOI identifier (e.g., 10.31410/Balkans.JETSS), not the full URL. Remove the ‘https://doi.org/‘ prefix.”

Scenario B: Duplicate DOI


// User enters: 10.31410/Balkans.JETSS.2025.8.2.114-1
// API returns:
{
  "doi": ["The given DOI suffix is already in use for another published item. Please enter a unique DOI suffix for each item."]
}

But the UI shows: “Some DOI(s) could not be updated”

No indication of:

  • ❌ Which article already has this DOI
  • ❌ Where to find the conflicting DOI
  • ❌ How to resolve the conflict
  • Problem #2: Invisible Error Messages

The real errors only appear in browser DevTools:

  1. Open browser DevTools (F12)
  2. Go to Network tab
  3. Click “Save” on DOI form
  4. Click the failed request (red, status 400)
  5. View Response tab

There you’ll find the ACTUAL error message.

The Root Cause: Lazy Frontend Development

Looking at the OJS source code:

Backend (API) – Works Correctly:


// File: lib/pkp/api/v1/dois/PKPDoiHandler.php

$errors = Repo::doi()->validate(null, $params);

if ($errors) {
    return $response
        ->withStatus(400)
        ->withJsonError($errors); // ✓ Returns detailed errors
}

Frontend (JavaScript) – Broken:


// File: pages/dois/DoisHandler.php (Vue component)

.catch(error => {
  // ❌ Shows generic message instead of actual error
  this.showError('Some DOI(s) could not be updated');
  // Should be:
  // this.showError(error.response.data.doi[0]);
});

Proposal for OJS upgradeS

  1. Parse API error responses properly
  2. Add format validation before submission
  3. Show conflicting article when duplicate detected

Final Results

✅ OJS upgraded: 3.3.0.9 → 3.4.0.10 (stable)

✅ DOIs migrated: All DOIs successfully recovered

✅ Database integrity: All tables and relationships validated

✅ DOI management: Fully functional

✅ User training: UDekom team trained

✅ Documentation: Complete technical documentation provided

And last but not the least:

✅ We hope that we helped somebody by publishing the issue we faced and possible solutions.

We reached out to multiple consultants and hosting providers — nobody could solve our DOI problem. Most gave up after seeing the complexity of our database state. The We-Deliver.io team not only fixed our broken upgrade but also recovered DOIs we thought were lost forever. Their technical expertise and persistence saved our journal’s credibility. I highly recommend them for any serious OJS work — Nikolina Vrcelj

Nikolina Vrcelj

Manager, UDEKOM BALKANS

Use our WP Crossref DOI automated import plugin

If your organization could benefit from similar workflow automation or IT solutions, feel free to reach out—We Deliver IT specializes in turning tedious processes into streamlined systems that let you focus on what truly matters.

About the Author

Stojan Ivanisevic, PhD is the founder and Solutions Architect of We Deliver IT. He is a seasoned IT professional with over 17 years of experience in roles ranging from software developer to strategic consultant. Stojan has led the development of critical systems for government institutions, international organizations, and innovative companies worldwide. With a strong academic background (holding a PhD in Information Technologies) and an eye for practical solutions, he excels at turning complex requirements into clear, functional outcomes. Stojan is known for delivering projects “on time, on budget, and in quality,” upholding the very promise in his company’s name.

SEO Note: For multilingual readers, this article will also be available in Serbian. To read the article in Serbian click here.