Migrating Alan application data

  1. Introduction
  2. Starting with an empty dataset
  3. Carrying data over
  4. The migration language
  5. Keeping a migration up to date
  6. Generating a migration

Introduction

The data of an Alan application always matches its model. That is what makes the app dependable — and it is also what makes a model change interesting: as soon as application.alan changes, the data of the running app no longer matches it.

A migration bridges that gap. It is a file that says, for every piece of data the new model expects, where that value comes from: copied from the running app, converted, or created new. The compiler checks it against both models, so a deployment either carries the data over completely or does not happen at all.

Every deployment therefore starts from a dataset, and there are two ways to get one:

This guide covers both, and the language migrations are written in. It assumes the project layout of the online Alan IDE, as described in the IDE tutorial.

Returning after a while? Since platform version 2026.1 a migration is written in the processor language of the Alan connector. The language changed completely: no typed declarations, no map, and no error annotations between <! and !>.

Starting with an empty dataset

Alan Deploy asks which data source the deployment should use. A first deployment has nothing to migrate from, so pick empty:

Choosing a data source for the deployment

That deployment creates the folder migrations/from_empty:

The generated from_empty migration

The folder is a complete migration project:

Since there is no source data, the generated migration.alan sets every collection to none, the empty collection:

root = root as $ {
	(
		'Menu' = none
	)
}

To start the app with data instead, replace those none expressions with create entries. This migration gives the Menu of the restaurant tutorial three items:

root = root as $ {
	(
		'Menu' = {
			create (
				'Item name' = "Shrimp salad"
				'Selling price' = 4
			)
			create (
				'Item name' = "Tomato soup"
				'Selling price' = 5
			)
			create (
				'Item name' = "Orange juice"
				'Selling price' = 5
			)
		}
	)
}

One thing to watch: migrations/from_empty is regenerated on every deployment with the empty option, so anything you write there is overwritten. Keep hand-written initial data somewhere else and copy it in when you need it — which is exactly what the restaurant tutorial does with the migration.alan files in its _docs folder.

Carrying data over

Once an app is running, a deployment can take its data along. Change the model, build, and choose migrate:

Migrating from the running version

The first time, this creates migrations/from_release. Unlike from_empty, that folder is generated only once: its migration.alan is yours to maintain and survives every following deployment.

models/source/application.alan in that folder is the model of the deployed app, and the IDE refreshes it at every migrate deployment. Do not edit it — it describes what the running app holds, which is not yours to decide. (If you do need a source model of your own, for instance to read a derived value that the deployed model computes, copy the whole from_release folder under a different name and edit the copy.)

The migration language

A migration.alan file gives an expression for every base data property of the target model. Derived values are not migrated: the app recomputes those from the data itself.

The language is the processor language of the Alan connector; its grammar lists every operation. The rest of this section is what a typical application needs.

The example below migrates the first step of the restaurant tutorial — a Menu whose Selling price is in whole euros — to the second step, where the price is in eurocents and every item has an Item type:

root = root as $ {
	(
		'Menu' = walk $ .'Menu' as $ => {
			create (
				'Item name' = $ .'Item name'
				'Selling price' = product ( $ .'Selling price' , 100 )
				'Item type' = create 'Dish' (
					'Dish type' = create 'Main course' ( )
				)
			)
		}
	)
}

The shape of a migration. It starts with root = root as $ {. The $ is the source: the data of the running app, conforming to models/source/application.alan. Braces { ... } hold a block, and the parentheses ( ... ) inside a block hold the properties of one node of the target model, each with an expression for its value.

Copying values. $ .'Item name' reads a property of the current source node. Text and number values can be copied like that whenever their type did not change, and a reference is migrated as the text of the key it points at, so 'Item' = $ .'Item' works for a reference too.

Collections. Walk the source collection and create one target entry per source entry:

'Menu' = walk $ .'Menu' as $ => {
	create (
		'Item name' = $ .'Item name'
		'Selling price' = product ( $ .'Selling price' , 100 )
		'Item type' = create 'Dish' (
			'Dish type' = create 'Main course' ( )
		)
	)
}

Inside the walk, $ is the current source entry, so $ .'Item name' now reads from that entry. A collection without a source is written as a block of create entries, as in the from_empty example above, or as none when it should start empty.

Stategroups. Switch on the source stategroup and create the matching target state in every case:

'Cargo' = switch $ .'Cargo' (
	|'Container' as $ => {
		create 'Container' (
			'Slots' = $ .'Slots'
		)
	}
	|'Bulk' => {
		create 'Bulk' ( )
	}
)

Each case binds $ to the state’s node with as $, which is how 'Slots' above reads a property that only exists in that state. A stategroup that is new in the target model has nothing to switch on, so create a fixed state:

'Item type' = create 'Dish' (
	'Dish type' = create 'Main course' ( )
)

Groups. A group is a node, so it is a block with parentheses in it:

'Fleet' = {
	(
		'Name' = $ .'Fleet name'
		'Default rate' = $ .'Default rate'
	)
}

Numbers. A number is migrated as an integer in the unit of the target numerical type. When that unit changes, convert the value with product or division — the language has no * and / operators. From euros to eurocents:

'Selling price' = product ( $ .'Selling price' , 100 )

Literals. Text goes between double quotes ("Example"), a number is a plain integer (2042), and none is the empty collection.

Reaching data further up. Inside a walk or a switch, $ is the innermost source node, so properties higher up in the source are out of reach. Give a node a name with let, and use that name wherever you need it, however deep:

let $'source' = $

(
...
'Rate' = $'source' .'Default rate'

Here every charter takes the fleet-wide Default rate from the root of the source, while $ inside the walk still refers to the charter being migrated.

Keeping a migration up to date

from_release/migration.alan has to describe a source for every base data property in your model. Change the model and deploy with migrate, and the migration is compiled against the new target model: the deployment fails until every new or changed property has a valid expression. That failure is the feature — it is the platform refusing to put data into an app that does not fit it.

A generated migration (see Generating a migration) is a starting point, not a finished one: it assumes the source model has the same structure as the target and copies everything. For the model change of the previous section, the generator produces:

root = root as $ {
	(
		'Menu' = walk $ .'Menu' as $ => {
			create (
				'Item name' = $ .'Item name'
				'Selling price' = $ .'Selling price'
				'Item type' = switch $ .'Item type' (
					|'Dish' as $ => {
						create 'Dish' (
							'Dish type' = switch $ .'Dish type' (
								|'Appetizer' as $ => {
									create 'Appetizer' ( )
								}
								|'Main course' as $ => {
									create 'Main course' ( )
								}
								|'Dessert' as $ => {
									create 'Dessert' ( )
								}
							)
						)
					}
					|'Beverage' as $ => {
						create 'Beverage' (
							'Beverage type' = switch $ .'Beverage type' (
								|'Juice' as $ => {
									create 'Juice' ( )
								}
								|'Soft drink' as $ => {
									create 'Soft drink' ( )
								}
								|'Cocktail' as $ => {
									create 'Cocktail' ( )
								}
								|'Beer' as $ => {
									create 'Beer' ( )
								}
								|'Wine' as $ => {
									create 'Wine' ( )
								}
								|'Coffee & tea' as $ => {
									create 'Coffee & tea' ( )
								}
							)
						)
					}
				)
			)
		}
	)
}

Compiling that fails, because the source model has no Item type yet:

‘property’ Item type was not found in ‘attributes’. Existing ‘attributes’: Item name, Selling price

Two edits fix it, both shown in the complete migration above: the switch on $ .'Item type' becomes a create of a fixed state, and Selling price gets its conversion to eurocents.

A second example. For an application.alan with

root {
	'App Name': text
	'App Description': text
	'Users': collection ['User'] {
		'User': text
	}
	'Year': number 'year'
}

a valid migration from a model that only had an Original App Name is:

root = root as $ {
	(
		'App Name' = $ .'Original App Name'
		'App Description' = "Example app for migrations."
		'Users' = none
		'Year' = 2042
	)
}

Note what each line does: App Name is renamed, App Description is a value chosen here and now, Users starts empty, and Year gets a literal. New data has to come from somewhere, and a migration is where you decide from where.

When source data can be missing. Some expressions can fail: a lookup in a collection with [ ... ] fails when the key is not there, and following a reference can fail for the same reason. Suppose the target model turns an Administrator text into a reference to Users, and adds the administrator’s name:

root {
	'Users': collection ['User'] {
		'User': text
		'Full name': text
	}
	'Administrator': text -> .'Users'[]
	'Administrator name': text
}

Either stop the migration with a message of your own:

'Administrator' = $ .'Users'[ $ .'Administrator' ] .'User' || throw "The administrator is not a known user."

which appears in the Output window when the deployment fails, or handle both outcomes with a switch that distinguishes value from none:

'Administrator name' = switch $ .'Users'[ $ .'Administrator' ] (
	| value as $ => $ .'Full name'
	| none => "unknown"
)

The second form is the one to reach for when missing data is normal rather than exceptional.

Generating a migration

The IDE can write the mechanical part of a migration for you: run Alan: Generate Migration from the command palette.

It asks for three things:

Generate under a new name, or move the existing folder aside first, so that a migration.alan you edited by hand is not overwritten.

Running it again after a model change is a useful habit: it gives you the expressions for the properties you just added, which you copy into from_release/migration.alan and then adjust — usually only to say where their initial values come from.

The same generator is available from the terminal:

.alan/devenv/system-types/datastore/scripts/generate_migration.sh migrations/from_release models/model
.alan/devenv/system-types/datastore/scripts/generate_migration.sh migrations/from_empty models/model --strategy bootstrap

To check your migrations without deploying, run ./alan build -C migrations from the terminal; it compiles every migration project in the migrations folder.