Development of a 1C-Bitrix database migration module

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.