1C-Bitrix Database Migration Module Development
1C-Bitrix does not have a built-in migration system in the sense of Rails Migrations or Laravel Migrations. Schema changes accumulate in developers' heads, in README files, or nowhere at all — and something breaks when deploying to production. A dedicated migration module solves this problem systematically.
Why Standard Tools Are Not Enough
The built-in Bitrix update mechanism (/bitrix/modules/<module>/install/db/mysql/install.sql) is designed for fresh module installation, not for incremental changes. Adding a column to a table, changing a column type, or creating an index is done either manually in phpMyAdmin or via a script that is run once by hand. Reproducing the change history on a test environment becomes a non-trivial challenge.
An additional complication: Bitrix actively uses both its own internal tables (b_*) and custom user tables. The migration module must be able to work with both without conflicting with platform updates.
Migration Module Architecture
The module is implemented as a full-fledged 1C-Bitrix module in the /bitrix/modules/vendor.migrations/ directory. Structure:
vendor.migrations/
├── install/
│ ├── index.php # Module installer
│ └── db/
│ └── mysql/
│ └── install.sql # Migration history table
├── lib/
│ ├── Migration.php # Base migration class
│ ├── Runner.php # Run and rollback
│ └── Repository.php # Migration file discovery
└── migrations/ # Directory with migration files
The history table stores information about applied migrations:
CREATE TABLE `b_vendor_migrations` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`MIGRATION` varchar(255) NOT NULL,
`APPLIED_AT` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`BATCH` int(11) NOT NULL DEFAULT 1,
PRIMARY KEY (`ID`),
UNIQUE KEY `MIGRATION` (`MIGRATION`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
The BATCH field allows rolling back a group of migrations with a single command — everything applied in one deployment.
Base Class and Migration API
namespace Vendor\Migrations;
use Bitrix\Main\Application;
abstract class Migration
{
protected $db;
public function __construct()
{
$this->db = Application::getConnection();
}
abstract public function up(): void;
abstract public function down(): void;
protected function addColumn(string $table, string $column, string $definition): void
{
$sql = "ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}";
$this->db->query($sql);
}
protected function addIndex(string $table, string $name, array $columns, bool $unique = false): void
{
$type = $unique ? 'UNIQUE INDEX' : 'INDEX';
$cols = implode('`, `', $columns);
$this->db->query("ALTER TABLE `{$table}` ADD {$type} `{$name}` (`{$cols}`)");
}
}
A concrete migration looks like this:
// migrations/2024_03_15_001_add_region_to_orders.php
class Migration_2024_03_15_001_add_region_to_orders extends \Vendor\Migrations\Migration
{
public function up(): void
{
$this->addColumn('b_sale_order', 'REGION_ID', 'int(11) NULL DEFAULT NULL');
$this->addIndex('b_sale_order', 'idx_region', ['REGION_ID']);
}
public function down(): void
{
$this->db->query("ALTER TABLE `b_sale_order` DROP INDEX `idx_region`");
$this->db->query("ALTER TABLE `b_sale_order` DROP COLUMN `REGION_ID`");
}
}
Runner: Apply and Rollback
Runner::run() scans the migrations/ directory, compares with the history table, and applies pending migrations in chronological order. Transactions are mandatory — if a migration fails halfway through, the database must not be left in an intermediate state.
public function run(): array
{
$pending = $this->repository->getPending();
$batch = $this->getNextBatch();
$applied = [];
foreach ($pending as $migration) {
$this->db->startTransaction();
try {
$instance = new $migration();
$instance->up();
$this->markAsApplied($migration, $batch);
$this->db->commitTransaction();
$applied[] = $migration;
} catch (\Exception $e) {
$this->db->rollbackTransaction();
throw $e;
}
}
return $applied;
}
CI/CD Integration
The module integrates into the CI/CD pipeline: after code is deployed to the server, migrations are run automatically. For Bitrix projects this is typically done via php -r "require('/var/www/bitrix/modules/main/include/prolog_before.php'); \Vendor\Migrations\Runner::getInstance()->run();" as part of the deploy script.
Alternatively — via a Bitrix agent or a dedicated administrative section with a manual run button and a log.
Info Blocks and User Fields
Migrations for info blocks are a separate class of complexity. Adding an info block property directly through SQL bypasses the Bitrix cache. The correct approach is to use ORM methods in up():
$prop = new \CIBlockProperty();
$prop->Add([
'IBLOCK_ID' => $this->getIblockId('catalog'),
'CODE' => 'VENDOR_CODE',
'NAME' => 'Supplier article number',
'PROPERTY_TYPE' => 'S',
'ACTIVE' => 'Y',
]);
Typical Development Timeline
| Configuration | Duration |
|---|---|
| Base module: up/down, history, CLI | 2–3 weeks |
| + Administrative interface, log | +1 week |
| + Info block and UF field support | +1 week |
| + CI/CD integration, documentation | +3–5 days |
The module is delivered with a license agreement, migration authoring documentation, and examples for different types of schema changes.







