Skip to main content

๐Ÿ”„ Migrations

Migrations allow you to evolve your data schema over time without losing existing player data.


๐Ÿ“š What Are Migrations?โ€‹

When you change your dataTemplate, existing saved data won't automatically have the new structure. Migrations are functions that transform old data into the new format.

flowchart LR
A["๐Ÿ“ฆ Old Data<br/>v1"] --> B["๐Ÿ”„ Migration 2"]
B --> C["๐Ÿ”„ Migration 3"]
C --> D["โœ… Current Data<br/>v3"]

๐Ÿ› ๏ธ Creating Migrationsโ€‹

Add migration functions to your schema's migrations array:

local PlayerSchema = Coppermind.registerSchema({
name = "PlayerData",
dataTemplate = {
coins = 0,
gems = 0,
diamonds = 0, -- Added in v2
settings = { -- Added in v3
musicEnabled = true,
},
},
migrations = {
-- Migration 1: Initial version (often empty)
function(data)
-- Nothing to do for v1
end,

-- Migration 2: Add diamonds field
function(data)
data.diamonds = 0
end,

-- Migration 3: Add settings
function(data)
data.settings = {
musicEnabled = true,
}
end,
},
})

โš™๏ธ How Migrations Workโ€‹

StepDescription
1๏ธโƒฃEach saved data entry has a version number
2๏ธโƒฃWhen loading, Coppermind compares saved version to migration count
3๏ธโƒฃAll migrations from savedVersion + 1 to currentVersion run
4๏ธโƒฃData is reconciled with the template

Example Flowโ€‹

๐Ÿ“ฆ Saved data (version 1):
{ coins = 500 }

๐Ÿ”„ Migrations 2 and 3 run:
{ coins = 500, diamonds = 0, settings = { musicEnabled = true } }

โœ… Reconciliation with template:
{ coins = 500, gems = 0, diamonds = 0, settings = { musicEnabled = true } }

โœ… Migration Best Practicesโ€‹

Keep Forever

Once deployed, migrations should never be removed!

migrations = {
function(data) end, -- v1 - keep forever
function(data)
data.newField = 0
end, -- v2 - keep forever
function(data)
data.anotherField = ""
end, -- v3 - keep forever
}

๐Ÿงฉ Complex Migration Examplesโ€‹

๐Ÿ“‚ Restructuring Dataโ€‹

-- Before: { level = 5, xp = 100, ... }
-- After: { stats = { level = 5, xp = 100 }, ... }

function(data)
data.stats = {
level = data.level or 1,
xp = data.xp or 0,
}
data.level = nil
data.xp = nil
end

๐Ÿ“‹ Converting Array Formatsโ€‹

-- Before: { inventory = { "sword", "shield" } }
-- After: { inventory = { { id = "sword", count = 1 }, { id = "shield", count = 1 } } }

function(data)
if data.inventory and type(data.inventory[1]) == "string" then
local newInventory = {}
for _, itemId in data.inventory do
table.insert(newInventory, {
id = itemId,
count = 1,
})
end
data.inventory = newInventory
end
end

๐Ÿงฎ Adding Computed Defaultsโ€‹

-- Set initial value based on existing data
function(data)
-- Veterans get a bonus
if data.playtime and data.playtime > 3600 then
data.veteranBonus = 100
else
data.veteranBonus = 0
end
end

๐Ÿงช Testing Migrationsโ€‹

Use Mock Mode

Test migrations without affecting real player data!

Coppermind.setMockMode(true)
Coppermind.clearMockData()

-- Simulate old data
local store = Coppermind.loadStore(PlayerSchema, "test_key", {})
task.wait(0.2)

-- Verify migrations ran correctly
local data = Coppermind.getData(PlayerSchema, "test_key")
assert(data.newField ~= nil, "Migration should add newField")

Coppermind.unloadStore(PlayerSchema, "test_key")
Coppermind.setMockMode(false)

๐Ÿ“Š Version Trackingโ€‹

The store's version property tracks which migrations have been applied:

local store = Coppermind.loadStore(schema, key, {})

store.onReady:Connect(function()
print("Data version:", store.version)
-- This equals the number of migrations that have been applied
end)

๐Ÿ› Debugging Migrationsโ€‹

Development Only

Remove debug logging before deploying to production!

migrations = {
function(data)
print("[Migration 1] Running...")
data.newField = data.newField or "default"
print("[Migration 1] Complete")
end,
function(data)
print("[Migration 2] Running...")
print("[Migration 2] Current data:", data)
data.anotherField = true
print("[Migration 2] Complete")
end,
}