NAME
DBIO::Manual::Migration - Step-by-step guide for migrating an existing DBIx::Class codebase to DBIO
VERSION
version 0.900004
OVERVIEW
DBIO is a clean break from DBIx::Class. There is no runtime compatibility shim. Migration is a mechanical, reviewable, code-level transformation: rename packages, swap a small number of APIs, drop a few removed features.
This guide walks through that transformation step by step. Each step is independent and testable. After all steps, your application talks to DBIO and the door to DBIx::Class is closed.
The full DBICTest schema set from DBIx::Class 0.082844 has been imported under t/lib/MigrationsTest/ as a reference fixture. The conversion below is what was actually applied to it.
For the rationale and full inventory of what changed, see DBIO::Manual::Heritage. This document is the how; Heritage is the what and why.
STEP 1 - RENAME THE NAMESPACE
Replace every occurrence of DBIx::Class with DBIO across your codebase. This is a pure mechanical transform:
DBIx::Class -> DBIO
DBIx::Class::Core -> DBIO::Core
DBIx::Class::Row -> DBIO::Row
DBIx::Class::Schema -> DBIO::Schema
DBIx::Class::ResultSet -> DBIO::ResultSet
DBIx::Class::ResultSource::Table -> DBIO::ResultSource::Table
DBIx::Class::ResultSource::View -> DBIO::ResultSource::View
DBIx::Class::ResultClass::HashRefInflator -> DBIO::ResultClass::HashRefInflator
DBIx::Class::Storage::DBI -> DBIO::Storage::DBI
A typical sweep:
find lib t -name '*.pm' -o -name '*.pl' -o -name '*.t' \
| xargs sed -i 's/DBIx::Class/DBIO/g'
This handles 95 % of a vanilla codebase. After the sweep, fix up the two namespaces whose names changed shape, not just prefix:
DBIO::_Util -> DBIO::Util (was DBIx::Class::_Util)
DBIO::_ENV_ -> drop (was DBIx::Class::_ENV_, machinery removed)
The remaining steps cover the cases where a like-for-like rename is wrong because the API itself has changed.
STEP 2 - DROP CDBICompat AND OTHER REMOVED COMPONENTS
The following components are gone with no replacement:
DBIx::Class::CDBICompat - Class::DBI bridge. Port to native DBIO Result classes.
DBIx::Class::Schema::Versioned - removed; use dbioadmin's native deploy / upgrade (see DBIO::Manual::Heritage).
The SQL::Abstract::Classic escape hatch - DBIO uses canonical SQL::Abstract only. (
DBIx::Class::SQLMaker::ClassicExtensionslives on as DBIO::SQLMaker::ClassicExtensions, a mixin inside DBIO::SQLMaker, not a component to load.)
Remove these from load_components() calls and any connect_info options that referenced them.
STEP 3 - REPLACE INTEGRATED COMPONENTS
A handful of widely-used CPAN components have been folded into core DBIO. Their independent CPAN versions still install but no longer do anything useful inside DBIO. Switch to the integrated module:
DBIx::Class::TimeStamp -> DBIO::Timestamp
DBIx::Class::UUIDColumns -> DBIO::UUIDColumns
DBIx::Class::Helpers -> integrated into DBIO::Row
DBIx::Class::IntrospectableM2M -> DBIO::Relationship::ManyToMany (always on)
In load_components these names are unchanged after step 1 except for case (DBIO uses Timestamp, not TimeStamp):
__PACKAGE__->load_components(qw/Timestamp UUIDColumns/);
DBIx::Class::PassphraseColumn has no DBIO counterpart. One-way encoded columns are DBIO::EncodedColumn (the encode_column column flag), which writes its own storage format.
STEP 4 - SWITCH CLASSDATA APIS TO COLUMN FLAGS
Several components used class-level configuration calls. DBIO routes the same information through column-info flags on add_columns. This makes the per-column intent visible at the column declaration and removes one layer of side-effect-at-load-time.
Ordered
# before
__PACKAGE__->position_column('position');
__PACKAGE__->grouping_column('cd');
__PACKAGE__->null_position_value(undef);
__PACKAGE__->add_columns(
position => { data_type => 'integer' },
cd => { data_type => 'integer' },
);
# after
__PACKAGE__->add_columns(
position => { data_type => 'integer', position => 1 },
cd => { data_type => 'integer', grouping => 1 },
);
# null_position_value goes on the position column:
# position => { ..., position => 1, null_position_value => undef }
The class-level methods position_column, grouping_column and null_position_value still exist but are now read-only introspection helpers; setters are gone.
Timestamp
# before
__PACKAGE__->add_columns(
created => { data_type => 'datetime' },
updated => { data_type => 'datetime' },
);
# configuration was implicit via the component name; columns named
# created/updated were auto-detected.
# after - explicit per-column flags
__PACKAGE__->add_columns(
created => { data_type => 'datetime', set_on_create => 1 },
updated => { data_type => 'datetime', set_on_create => 1, set_on_update => 1 },
);
# or the helper:
__PACKAGE__->cols_updated_created;
UUIDColumns
# before
__PACKAGE__->uuid_columns('artist_id');
# after
__PACKAGE__->add_columns(
artist_id => { data_type => 'varchar', size => 36, uuid_on_create => 1 },
);
ChangeLog
# before
__PACKAGE__->changelog_exclude_columns(qw/ password_hash /);
# after
__PACKAGE__->add_columns(
password_hash => { data_type => 'varchar', size => 255, changelog => 0 },
);
STEP 5 - REPLACE LIMIT DIALECT MACHINERY
DBIx::Class drove LIMIT/OFFSET emulation through a string dispatch (sql_limit_dialect) and an emulate_limit() hook. DBIO replaces both with a single overridable method on the SQLMaker subclass:
package MyApp::SQLMaker;
use base 'DBIO::SQLMaker';
sub apply_limit {
my ($self, $sql, $rs_attrs, $rows, $offset) = @_;
# append $self->_parse_rs_attrs($rs_attrs) and the dialect-specific
# LIMIT clause; push its bind values onto $self->{limit_bind}
return $sql;
}
$sql arrives without its GROUP BY, HAVING and ORDER BY clauses: apply_limit renders them from $rs_attrs with _parse_rs_attrs and places them itself. The bind values of the limit clause go onto $self->{limit_bind}, each as [ $self->__rows_bindtype => $rows ] or [ $self->__offset_bindtype => $offset ], and the method returns the SQL string only.
If you had sql_limit_dialect = 'GenericSubQ' or similar, port the behaviour into apply_limit. The default in core DBIO is LIMIT ? OFFSET ?, which the PostgreSQL, SQLite and DuckDB drivers use as it is; a driver whose database needs another syntax (MySQL, Oracle, MSSQL, DB2, Firebird, Sybase) overrides it.
STEP 6 - REPLACE SQL::Abstract::Classic CALLS
If you reached into SQL::Abstract::Classic directly (rare), replace with SQL::Abstract 2.000001 or newer. The query DSL is compatible. Differences are in internal extension points only.
STEP 7 - UPDATE TEST INFRASTRUCTURE
Test environment variables and helper namespaces were renamed:
DBICTEST_* -> DBIO_TEST_*
DBICTest::* -> DBIO::Test::*
Driver-specific test result classes belong in the driver's lib/ namespace, not in t/lib/. Load them via the hashref form of "load_classes" in DBIO::Schema:
DBIO::Test::Schema->load_classes({
'DBIO::PostgreSQL::Test' => ['SequenceTest'],
});
Shared schemas for cross-distribution features go under DBIO::Test::Schema::*. Never redefine the same schema class inline in multiple driver tests.
STEP 8 - SCHEMA DEPLOYMENT
Every DBIO driver ships native introspect/diff/deploy modules; none requires SQL::Translator, which is no longer part of the core deploy path. $schema->deploy checks the active storage for a dbio_deploy_class() and routes to that native class:
$schema->deploy; # routed via dbio_deploy_class on the storage
If a storage declares no native deploy class, deploy() does not translate the schema for you - it falls through to deployment_statements(), which reads a pre-existing DDL file and throws if none is present. The core deploy path never calls SQL::Translator.
If your application explicitly invoked deployment_statements or parsed SQL::Translator output, review whether you still need that plumbing. The native deploy classes - DBIO::PostgreSQL::Deploy, DBIO::MySQL::Deploy, DBIO::SQLite::Deploy - expose install, diff, apply and upgrade directly. apply and upgrade run a destructive diff (drops, possibly lossy type changes) with allow_destructive => 1, and dry_run => 1 shows a diff without running it; see "apply" in DBIO::Deploy::Base.
STEP 9 - PORT CUSTOM COMPONENTS
If you wrote your own component using the column-info flag pattern (intercepting add_columns, stashing private flags, processing at lifecycle time), it should run unchanged after step 1. If you wrote a component using class-level setters (mk_classdata), consider porting it to the column-info flag pattern: see the four core components - DBIO::Timestamp, DBIO::Ordered, DBIO::ChangeLog, DBIO::UUIDColumns - for the reference idiom.
STEP 10 - DROP CONTEXT-SENSITIVE RETURN TYPES
DBIO methods no longer change their return type based on call context (ADR 0042): search and its family, the has_many/many_to_many accessors, slice and ResultSetColumn::func always return a ResultSet (or a scalar) and throw when called in list context. In DBIx::Class these returned the matching rows in list context:
# DBIx::Class: rows
my @cds = $cd_rs->search({ year => 2001 });
# DBIO: throws - fetch explicitly
my @cds = $cd_rs->search({ year => 2001 })->all;
The same applies to my @cds = $artist-cds> (has_many accessor; use $artist->cds->all), search_related, search_literal, search_like and slice. The *_rs accessors remain as aliases. func lost its list-context (one value per row) shape entirely; per-row values come from func_rs($fn)-all>.
Replacements for common idioms:
my @rows = $rs->search($cond, $attrs); # ->search($cond, $attrs)->all
my @rows = $rs->search_related($rel, $cond); # ->search_related(...)->all
my @rows = $obj->cds; # $obj->cds->all
STEP 11 - RUN THE TESTS
After each step, run the full test suite:
dzil test
If a step exposes failures that a single targeted edit can fix, do the edit and continue. If the step lands you on a real semantic change (e.g. an emulate_limit override that needs new logic), freeze it as a separate commit so the migration history reads cleanly.
REFERENCE FIXTURE
The file tree under t/lib/MigrationsTest/ is an import of DBIx::Class's t/lib/DBICTest/ with the namespace rewritten DBICTest -> MigrationsTest and the steps above applied. It holds the converted result only: no DBIx::Class::* reference is left in it, and the original DBICTest sources are not retained anywhere in the tree.
The tree is a reference; no test loads it. The schema the DBIO tests run against is DBIO::Test::Schema, with its result classes under DBIO::Test::Schema::*.
SEE ALSO
- DBIO::Manual::Heritage - what changed and why
- DBIO::Manual::Component - writing components against DBIO
- DBIO::Timestamp, DBIO::Ordered, DBIO::ChangeLog, DBIO::UUIDColumns - reference column-info-flag components
AUTHOR
DBIO & DBIx::Class Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.