Skip to Content
QBCore docs – powered by Nextra 4
MigrationToolsDatabase Migration Tools

Database Migration Tools

Automated scripts and tools to help you safely migrate your QBCore database between versions. These tools handle schema updates, data migration, and validation to ensure your player data remains intact during upgrades.

Overview

Database migration is one of the most critical aspects of upgrading QBCore. These tools provide automated, tested, and reversible migration procedures to minimize downtime and data loss risks.

⚠️ Critical Warning

Always backup your database before running any migration scripts. Test migrations on a copy of your production database first.

Quick Start

1. Download Migration Tools

# Clone the migration toolkit git clone https://github.com/qbcore-framework/qb-migration-tools.git cd qb-migration-tools # Make scripts executable chmod +x scripts/*.sh

2. Configure Database Connection

# Copy example configuration cp config/database.example.conf config/database.conf # Edit configuration with your database details nano config/database.conf
# database.conf DB_HOST="localhost" DB_PORT="3306" DB_USER="your_username" DB_PASS="your_password" DB_NAME="your_database" BACKUP_DIR="/path/to/backups"

3. Run Pre-Migration Check

# Validate current database state ./scripts/pre-migration-check.sh

4. Execute Migration

# Run the migration (example: v1 to v2) ./scripts/migrate-v1-to-v2.sh

Migration Scripts

QBCore v1.0 to v2.0 Migration

Pre-Migration Script (pre-migration-v2.sql)

-- QBCore v1 to v2 Database Pre-Migration -- Run this BEFORE installing QBCore v2.0 -- Create backup table for rollback CREATE TABLE IF NOT EXISTS `players_backup_v1` LIKE `players`; INSERT INTO `players_backup_v1` SELECT * FROM `players`; -- Add new required columns ALTER TABLE `players` ADD COLUMN IF NOT EXISTS `phone_number` VARCHAR(20) DEFAULT NULL, ADD COLUMN IF NOT EXISTS `last_updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, ADD COLUMN IF NOT EXISTS `metadata` LONGTEXT DEFAULT NULL; -- Create indexes for performance CREATE INDEX IF NOT EXISTS `idx_phone_number` ON `players` (`phone_number`); CREATE INDEX IF NOT EXISTS `idx_last_updated` ON `players` (`last_updated`); CREATE INDEX IF NOT EXISTS `idx_citizenid` ON `players` (`citizenid`); -- Initialize metadata column UPDATE `players` SET `metadata` = '{}' WHERE `metadata` IS NULL; -- Validate money data format UPDATE `players` SET `money` = CASE WHEN JSON_VALID(`money`) = 0 THEN '{"cash": 0, "bank": 0, "crypto": 0}' ELSE `money` END; -- Add missing money types UPDATE `players` SET `money` = JSON_SET( `money`, '$.cash', COALESCE(JSON_EXTRACT(`money`, '$.cash'), 0), '$.bank', COALESCE(JSON_EXTRACT(`money`, '$.bank'), 0), '$.crypto', COALESCE(JSON_EXTRACT(`money`, '$.crypto'), 0) ) WHERE JSON_EXTRACT(`money`, '$.cash') IS NULL OR JSON_EXTRACT(`money`, '$.bank') IS NULL OR JSON_EXTRACT(`money`, '$.crypto') IS NULL;

Main Migration Script (migrate-v1-to-v2.sql)

-- QBCore v1 to v2 Database Migration -- Run this AFTER installing QBCore v2.0 -- Update job structure for new format UPDATE `players` SET `job` = JSON_SET( `job`, '$.type', CASE WHEN JSON_EXTRACT(`job`, '$.name') IN ('police', 'bcso', 'sahp') THEN 'leo' WHEN JSON_EXTRACT(`job`, '$.name') IN ('ambulance', 'doctor') THEN 'ems' WHEN JSON_EXTRACT(`job`, '$.name') = 'mechanic' THEN 'mechanic' ELSE 'civilian' END, '$.onduty', COALESCE(JSON_EXTRACT(`job`, '$.onduty'), true) ) WHERE JSON_EXTRACT(`job`, '$.type') IS NULL; -- Update gang structure for new format UPDATE `players` SET `gang` = JSON_SET( `gang`, '$.type', 'gang', '$.onduty', COALESCE(JSON_EXTRACT(`gang`, '$.onduty'), false) ) WHERE JSON_EXTRACT(`gang`, '$.type') IS NULL AND JSON_EXTRACT(`gang`, '$.name') != 'none'; -- Convert old phone numbers to new format UPDATE `players` SET `phone_number` = CONCAT('555-', LPAD(FLOOR(RAND() * 10000), 4, '0')) WHERE `phone_number` IS NULL; -- Ensure unique phone numbers SET @row_number = 0; UPDATE `players` SET `phone_number` = CONCAT('555-', LPAD((@row_number := @row_number + 1), 4, '0')) WHERE `phone_number` IS NULL OR `phone_number` = ''; -- Migrate character info to new format UPDATE `players` SET `charinfo` = JSON_SET( `charinfo`, '$.backstory', COALESCE(JSON_EXTRACT(`charinfo`, '$.backstory'), 'No backstory available'), '$.nationality', COALESCE(JSON_EXTRACT(`charinfo`, '$.nationality'), 'American'), '$.phone', COALESCE(JSON_EXTRACT(`charinfo`, '$.phone'), `phone_number`) ) WHERE JSON_EXTRACT(`charinfo`, '$.backstory') IS NULL; -- Create new tables for v2 features CREATE TABLE IF NOT EXISTS `player_outfits` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `citizenid` VARCHAR(50) NOT NULL, `outfit_name` VARCHAR(50) NOT NULL, `outfit_data` LONGTEXT NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_citizenid` (`citizenid`), CONSTRAINT `fk_player_outfits_players` FOREIGN KEY (`citizenid`) REFERENCES `players`(`citizenid`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `player_phone_messages` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `citizenid` VARCHAR(50) NOT NULL, `number` VARCHAR(20) NOT NULL, `messages` LONGTEXT NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_citizenid` (`citizenid`), KEY `idx_number` (`number`) ); -- Migration validation SELECT COUNT(*) as total_players, COUNT(CASE WHEN `metadata` IS NOT NULL THEN 1 END) as players_with_metadata, COUNT(CASE WHEN `phone_number` IS NOT NULL THEN 1 END) as players_with_phone, COUNT(CASE WHEN JSON_VALID(`money`) THEN 1 END) as players_with_valid_money FROM `players`;

Post-Migration Validation (post-migration-check.sql)

-- Post-migration validation script -- Verify data integrity after migration -- Check for missing required data SELECT 'Missing phone numbers' as issue, COUNT(*) as count FROM `players` WHERE `phone_number` IS NULL OR `phone_number` = '' UNION ALL SELECT 'Invalid money data' as issue, COUNT(*) as count FROM `players` WHERE JSON_VALID(`money`) = 0 UNION ALL SELECT 'Missing metadata' as issue, COUNT(*) as count FROM `players` WHERE `metadata` IS NULL UNION ALL SELECT 'Duplicate phone numbers' as issue, COUNT(*) - COUNT(DISTINCT `phone_number`) as count FROM `players` WHERE `phone_number` IS NOT NULL; -- Validate job structure SELECT JSON_EXTRACT(`job`, '$.name') as job_name, COUNT(*) as player_count, COUNT(CASE WHEN JSON_EXTRACT(`job`, '$.type') IS NULL THEN 1 END) as missing_type FROM `players` GROUP BY JSON_EXTRACT(`job`, '$.name') HAVING missing_type > 0; -- Check database performance ANALYZE TABLE `players`; SHOW INDEX FROM `players`;

Migration Script Usage

Automated Migration Tool

#!/bin/bash # migrate-qbcore.sh - Complete migration automation source config/database.conf # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo -e "${GREEN}QBCore Database Migration Tool${NC}" echo "==================================" # Function to execute SQL file execute_sql() { local file=$1 local description=$2 echo -e "${YELLOW}Executing: $description${NC}" if mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME < $file; then echo -e "${GREEN}✓ Success: $description${NC}" return 0 else echo -e "${RED}✗ Failed: $description${NC}" return 1 fi } # Create backup echo -e "${YELLOW}Creating database backup...${NC}" BACKUP_FILE="$BACKUP_DIR/qbcore-backup-$(date +%Y%m%d-%H%M%S).sql" mkdir -p $BACKUP_DIR if mysqldump -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME > $BACKUP_FILE; then echo -e "${GREEN}✓ Backup created: $BACKUP_FILE${NC}" else echo -e "${RED}✗ Backup failed! Aborting migration.${NC}" exit 1 fi # Pre-migration checks echo -e "${YELLOW}Running pre-migration checks...${NC}" if ! execute_sql "sql/pre-migration-check.sql" "Pre-migration validation"; then echo -e "${RED}Pre-migration checks failed. Please resolve issues before continuing.${NC}" exit 1 fi # Execute pre-migration script if ! execute_sql "sql/pre-migration-v2.sql" "Pre-migration schema updates"; then echo -e "${RED}Pre-migration failed. Restoring backup...${NC}" mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME < $BACKUP_FILE exit 1 fi # Execute main migration if ! execute_sql "sql/migrate-v1-to-v2.sql" "Main migration v1 to v2"; then echo -e "${RED}Migration failed. Restoring backup...${NC}" mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME < $BACKUP_FILE exit 1 fi # Post-migration validation if ! execute_sql "sql/post-migration-check.sql" "Post-migration validation"; then echo -e "${YELLOW}⚠ Post-migration validation found issues. Check output above.${NC}" fi echo -e "${GREEN}Migration completed successfully!${NC}" echo "Backup location: $BACKUP_FILE" echo "Please test your server thoroughly before removing the backup."

Manual Step-by-Step Migration

If you prefer manual control:

# Step 1: Create backup mysqldump -u username -p database_name > backup-$(date +%Y%m%d).sql # Step 2: Test connection mysql -u username -p database_name -e "SELECT COUNT(*) FROM players;" # Step 3: Run pre-migration mysql -u username -p database_name < sql/pre-migration-v2.sql # Step 4: Verify pre-migration mysql -u username -p database_name < sql/pre-migration-check.sql # Step 5: Run main migration mysql -u username -p database_name < sql/migrate-v1-to-v2.sql # Step 6: Validate results mysql -u username -p database_name < sql/post-migration-check.sql

Rollback Procedures

Automatic Rollback Script

#!/bin/bash # rollback-migration.sh source config/database.conf echo "QBCore Migration Rollback Tool" echo "==============================" # List available backups echo "Available backups:" ls -la $BACKUP_DIR/*.sql read -p "Enter backup file name (full path): " BACKUP_FILE if [ ! -f "$BACKUP_FILE" ]; then echo "Backup file not found: $BACKUP_FILE" exit 1 fi echo "WARNING: This will completely restore your database to the backup state." echo "All data created after the backup will be lost." read -p "Are you sure? (type 'YES' to continue): " confirm if [ "$confirm" = "YES" ]; then echo "Restoring database from backup..." mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME < $BACKUP_FILE echo "Database restored successfully." else echo "Rollback cancelled." fi

Selective Rollback

For partial rollbacks of specific changes:

-- Rollback specific migration changes -- Example: Restore original money format -- Restore from backup table UPDATE `players` p1 JOIN `players_backup_v1` p2 ON p1.citizenid = p2.citizenid SET p1.money = p2.money; -- Remove added columns (if needed) -- ALTER TABLE `players` DROP COLUMN `phone_number`; -- ALTER TABLE `players` DROP COLUMN `metadata`;

Migration Validation Tools

Data Integrity Checker

#!/bin/bash # validate-migration.sh echo "QBCore Migration Validation" echo "===========================" # Check player count consistency ORIGINAL_COUNT=$(mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME -se "SELECT COUNT(*) FROM players_backup_v1;") CURRENT_COUNT=$(mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME -se "SELECT COUNT(*) FROM players;") echo "Player count comparison:" echo "Original: $ORIGINAL_COUNT" echo "Current: $CURRENT_COUNT" if [ "$ORIGINAL_COUNT" -eq "$CURRENT_COUNT" ]; then echo "✓ Player count matches" else echo "✗ Player count mismatch!" fi # Check data completeness mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME << 'EOF' SELECT 'Phone Numbers' as check_type, COUNT(*) as total, COUNT(phone_number) as with_data, COUNT(*) - COUNT(phone_number) as missing FROM players UNION ALL SELECT 'Metadata' as check_type, COUNT(*) as total, COUNT(CASE WHEN metadata != '{}' THEN 1 END) as with_data, COUNT(CASE WHEN metadata = '{}' OR metadata IS NULL THEN 1 END) as missing FROM players UNION ALL SELECT 'Valid Money' as check_type, COUNT(*) as total, COUNT(CASE WHEN JSON_VALID(money) THEN 1 END) as with_data, COUNT(CASE WHEN NOT JSON_VALID(money) THEN 1 END) as missing FROM players; EOF

Performance Impact Analysis

-- Check query performance after migration -- Run BEFORE and AFTER migration, compare results -- Test common queries EXPLAIN SELECT * FROM players WHERE citizenid = 'ABC12345'; EXPLAIN SELECT * FROM players WHERE phone_number = '555-1234'; EXPLAIN SELECT * FROM players WHERE JSON_EXTRACT(job, '$.name') = 'police'; -- Check index usage SHOW INDEX FROM players; -- Analyze table statistics ANALYZE TABLE players; SHOW TABLE STATUS LIKE 'players';

Custom Migration Scripts

Creating Custom Migration Scripts

For custom resources or modifications:

-- Template for custom migration script -- custom-migration-template.sql -- Header comment with description -- Custom Migration: [Your Resource Name] -- Version: [From] to [To] -- Date: [Date] -- Description: [What this migration does] -- Create backup CREATE TABLE IF NOT EXISTS `your_table_backup` LIKE `your_table`; INSERT INTO `your_table_backup` SELECT * FROM `your_table`; -- Your migration logic here ALTER TABLE `your_table` ADD COLUMN `new_column` VARCHAR(255); UPDATE `your_table` SET `new_column` = 'default_value'; -- Validation SELECT COUNT(*) as total_records, COUNT(`new_column`) as records_with_new_column FROM `your_table`; -- Rollback instructions (commented) /* DROP TABLE `your_table`; RENAME TABLE `your_table_backup` TO `your_table`; */

Troubleshooting Migration Issues

Common Error Solutions

Error: “Table doesn’t exist”

# Check if you're using the correct database mysql -u username -p -e "USE your_database; SHOW TABLES;"

Error: “Duplicate key error”

-- Find and resolve duplicate data SELECT phone_number, COUNT(*) FROM players GROUP BY phone_number HAVING COUNT(*) > 1; -- Fix duplicates UPDATE players SET phone_number = CONCAT(phone_number, '-', id) WHERE phone_number IN ( SELECT phone_number FROM ( SELECT phone_number FROM players GROUP BY phone_number HAVING COUNT(*) > 1 ) dup );

Error: “JSON format invalid”

-- Find and fix invalid JSON SELECT citizenid, money FROM players WHERE JSON_VALID(money) = 0; -- Fix invalid JSON UPDATE players SET money = '{"cash": 0, "bank": 0, "crypto": 0}' WHERE JSON_VALID(money) = 0;

Migration Monitoring

Real-time Migration Monitoring

#!/bin/bash # monitor-migration.sh # Monitor migration progress while true; do echo "$(date): Checking migration progress..." # Count processed records PROCESSED=$(mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME -se " SELECT COUNT(*) FROM players WHERE phone_number IS NOT NULL; ") TOTAL=$(mysql -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASS $DB_NAME -se " SELECT COUNT(*) FROM players; ") PERCENTAGE=$((PROCESSED * 100 / TOTAL)) echo "Progress: $PROCESSED/$TOTAL ($PERCENTAGE%)" sleep 10 done

✅ Migration Success Checklist

  • • Database backup created and verified
  • • Migration scripts executed without errors
  • • Data integrity validation passed
  • • Performance tests show acceptable results
  • • Server starts and functions normally
  • • Player data accessible and accurate

Additional Resources

Last updated on