NAME
DBIO::Extension - Base class of a database extension (PostGIS, AGE, pgvector, ...) known to deploy, introspection and diff
VERSION
version 0.900004
SYNOPSIS
package MyApp::DB::Extension::Vector;
use base 'DBIO::Extension';
sub extension_interface { 1 }
sub name { 'vector' }
sub driver { 'DBIO::PostgreSQL' }
sub verdict {
my ($class, $change, $ctx) = @_;
return undef unless $change->{kind} eq 'column_type'
&& ($change->{old_info}{data_type} // '') =~ /\bvector\(/;
return { refuse => "column $change->{column_name}: the dimension of a vector is not converted" };
}
DESCRIPTION
A database extension adds to a database what the application's schema does not declare: types, functions, operators and, for some extensions, objects in the application's own namespaces that the engine does not record as the extension's. Deploy, introspection and diff need three answers for each extension a database holds: which objects belong to the application model and which to the extension, what the extension adds to an object of the model, and how a change of such an object is written or refused (ADR 0066).
A DBIO extension is a class deriving from this one. Every method has a default that does nothing, so an extension writes only what it needs. All methods are class methods; anything an extension knows about the database comes through the context $ctx (see "THE CONTEXT").
The list of extensions a schema has is built by DBIO::Extension::List, the phases and their merges are run by DBIO::Extension::Protocol, and DBIO::Deploy::Base runs the protocol around introspection and diff. The commutativity test for an extension set is DBIO::Test::ExtensionSet.
METHODS
extension_interface
sub extension_interface { 1 }
The extension interface version the class implements. Required: the default returns undef, which "check_interface" refuses.
name
sub name { 'postgis' }
The engine's own name of the extension: pg_extension.extname, the DuckDB extension name, the SQLite module name. One class per engine extension, one entry per name. The default is undef: a plain class that serves several names (the driver's class for extensions without rules) states none, and the name of the list's entry stands. When a class states a name that differs from the entry's, the list refuses.
driver
sub driver { 'DBIO::PostgreSQL' }
The driver the extension belongs to, as an assertion. The extension never determines the driver (ADR 0032 section 7): it is compared with the resolved driver by "check_driver" in DBIO::Extension::List, after the DSN has decided the driver and never at connect. The default undef asserts nothing.
inspect
sub inspect { my ($class, $ctx) = @_; ... } # phase 0
The only method with database access: $ctx->{dbh} is the handle the deploy works on. Returns the extension's state as a plain, copyable structure (AGE: { graphs => [ { name => ..., namespace => ... } ] }) or undef for none. Core keeps it per extension under its name ($ctx->{self}{state} in later phases, $ctx->{peers}{$name}{state} for the others) and the driver may put it into the model, read-only. A deploy never creates or drops it. Default: undef.
claims
sub claims { my ($class, $model, $ctx) = @_; ... } # phase 1
Returns an array reference of addresses (see "ADDRESSES") of objects that are the extension's and not the application's (AGE: [ schema => $graph_schema ] for each graph). $model is the driver's base model, as the driver's introspection or target build produced it. The method reads; it does not change the model. Default: [].
annotate
sub annotate { my ($class, $model, $ctx) = @_; ... } # phase 2
Returns an array reference of [ $address, $attribute, $value ] triples for objects that stay in the model (PostGIS: base_type, geometry_type, srid on a column whose type lives in the PostGIS schema). $model is the model after phase 1, so objects another extension claimed are no longer in it. $attribute is a plain string, $value a plain scalar (or a structure comparable as data). Default: [].
verdict
sub verdict { my ($class, $change, $ctx) = @_; ... } # phase 3
Asked by the driver for a change it is about to write. $change is the driver's description of one change; the driver's POD states its keys. The column type change that is the case today has the form
{ kind => 'column_type',
table => $table_key,
column_name => $column,
old_info => \%column_info, # incl. the annotations of phase 2
new_info => \%column_info }
$ctx->{models} holds the source and target model after phase 2. Returns
undef-- not mine, the extension has no opinion,{ using => $expr }-- the change is written with thisUSINGexpression (or the driver's equivalent), or{ refuse => $message }-- the change is refused, with this message.
Anything else is a malformed answer and throws. Default: undef.
activation
sub activation { my ($class, $ctx) = @_; ... }
Per connection, as data, not statements: a hash reference with the keys
load-- a library name, an array reference of them, or a hash reference{ $name => $library }. A bare library name stands for itself ('age'is{ age => 'age' }).search_path-- a schema name or an array reference of them.
No other key is accepted. The driver turns the merged activation of all active extensions into the statements of a new connection (PostgreSQL: LOAD 'age' and the search_path, the namespace of the connection first and the extensions' entries in name order after it). Default: undef, nothing.
storage_layer
sub storage_layer { 'DBIO::PostgreSQL::Age::Storage' }
The storage layer (ADR 0032) that carries the extension's runtime surface (AGE: cypher, create_graph), or undef. The driver registers it for a declared extension that names one. Default: undef.
components
sub components { 'DBIO::PostgreSQL::PostGIS' }
An optional class method outside interface version 1: the result-side parts of the extension, as a list of component class names (PostGIS: the geometry inflation component). The driver applies them to the schema for a declared extension, as load_components would. Default: the empty list. A new optional method with a do-nothing default does not change the interface version (see "INTERFACE VERSION"), so an extension that does not need it never mentions it.
interface_current
my $v = DBIO::Extension->interface_current;
The interface version this core implements.
interface_versions
my @accepted = DBIO::Extension->interface_versions;
The set of interface versions this core accepts. Until the family's first release exactly the current version; afterwards the current and the previous one for one release.
check_interface
DBIO::Extension->check_interface($class);
Throws a DBIO::Exception unless $class derives from this class and its extension_interface is in "interface_versions". The message names the extension, its version, core's set and the side to upgrade. Returns true.
THE INTERFACE
Identity
Phases
Every phase method receives the same input for every active extension, as a copy, and returns data. See "PHASES AND MERGES".
Beyond the interface
THE CONTEXT
$ctx carries, for one deploy and one connection, a fresh copy for every call:
dbh-
Only in "inspect". The database handle of the deploy.
peers-
The other active entries, name to
{ class, installed, version, schema, state }.installedis true when the database has the extension,versionandschemaare what the driver read (undefwhen it did not),stateis the peer's "inspect" result (undefwhile phase 0 runs and for a peer without state). The peers are how an extension regulates its interaction with another one, inside its own code. self-
The extension's own entry:
{ name, class, options, installed, version, schema, state }.optionsare the options of the declaration. driver-
A hash reference of the driver's helpers an extension may use. The driver's POD names them (PostgreSQL:
quote_ident,qualified_key, the address constructors). Core passes the hash through and promises nothing about its content. Not copied: it holds code references. connection_schema-
The namespace of the connection (ADR 0065), or
undef. restricted-
True when the deploy is restricted to the schemas it manages.
models-
Only in "verdict":
{ source => $model, target => $model }after phase 2.
PHASES AND MERGES
The phases run in a fixed sequence. Within a phase every extension reads the same input and returns data; core merges.
phase input of every extension merge conflict
0 inspect the connection per extension, by name none
1 claims the driver's base model union of addresses none
2 annotate the model after phase 1 union of triples same address+attribute,
two values: refuses
3 verdict the change, models of 2 at most one answer per change two different answers
refuse; any refuse refuses
the diff, all listed
activation the context the driver composes the driver refuses what
it cannot compose
Phases 1 and 2 run on both sides of a diff, the live model and the target model, with the same set. The list is ordered by name; the order decides only the sequence of messages and of statements that do not depend on each other. No result depends on it.
ADDRESSES
An address is the driver's: [ schema => $name ], [ table => $key ], [ column => $table_key, $column ], [ type => $key ] and so on. The driver's POD lists its kinds and its cascade (a claimed schema takes its tables, a table its columns, indexes, triggers, keys). Core treats an address as an opaque array reference of scalars: it sorts, de-duplicates and compares it, and hands it back to the driver unchanged.
WRITING AN EXTENSION
A third extension needs no change in core, in the driver or in another extension: a class, and the declaration naming it.
package MyApp::DB::Extension::Vector;
use base 'DBIO::Extension';
sub extension_interface { 1 }
sub name { 'vector' }
sub driver { 'DBIO::PostgreSQL' }
# ... the methods above that the extension needs ...
package MyApp::DB;
use DBIO Schema => -pg;
__PACKAGE__->pg_extensions(vector => { class => 'MyApp::DB::Extension::Vector' });
An extension with result-side parts (a column inflation component) names them in "components"; the driver applies them for a declared extension. It is optional and outside the interface version.
The rules for every extension, family or not:
- 1. One class per engine extension;
nameis the engine's name. - 2. It states
extension_interfaceanddriver, and derives fromDBIO::Extension. - 3. Only "inspect" touches the database. The other methods are functions of their arguments: no changes to the input, no state between calls, no globals.
- 4. It never determines the driver (ADR 0032 section 7), changes no
@ISAof a storage, Deploy or Introspect class, and registers nothing into a driver registry. - 5. It answers only for what is its own. Where another extension's presence changes its answer, it reads
$ctx->{peers}; it does not ask core for an order. - 6. It passes DBIO::Test::ExtensionSet over itself and the extensions it names as peers.
The test:
use DBIO::Test::ExtensionSet qw(extension_set_ok);
extension_set_ok(
driver => 'DBIO::PostgreSQL::Deploy',
extensions => [ 'MyApp::DB::Extension::Vector', 'DBIO::PostgreSQL::PostGIS::Extension' ],
source => $live_model_fixture,
target => $target_model_fixture,
changes => [ { kind => 'column_type', ... } ],
);
It runs the phases in every permutation of the set, asserts the same post-phase models, diff statements and activation, and the same output of every extension in every position, and fails when an extension changed its input.
INTERFACE VERSION
DBIO::Extension carries the interface version core implements ("interface_current") and the set it accepts ("interface_versions"). A version outside the set refuses when the driver resolves the list ("check_interface"), with a DBIO::Exception; there is no warning mode. A new optional method with a do-nothing default does not change the version; a changed signature, a changed meaning of a return value or a new required method does.
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.