NAME
DBIO::Introspect::Base - Base class for DBIO driver introspectors
VERSION
version 0.900004
DESCRIPTION
Base class for the driver-specific introspectors (DBIO::PostgreSQL::Introspect, DBIO::SQLite::Introspect, DBIO::MySQL::Introspect). Provides new, the dbh accessor, and the lazy model builder. Subclasses must implement _build_model.
ATTRIBUTES
dbh
The connected DBI database handle. Required.
METHODS
contract_version
my $v = $class->contract_version;
Returns $CONTRACT_VERSION, the compatibility version of this base class's public interface -- independent of the dist $VERSION and unaffected by a release that changes no contract shape. See "CONTRACT VERSION" above and ADR 0024.
new
my $intro = $class->new(dbh => $dbh);
Blesses the argument hash directly; no validation. dbh is required.
model
The introspected database model hashref. Built lazily on first access via _build_model, which every driver subclass implements (the base version dies). The shape varies by driver.
_aggregate_by
my $by_table = $class->_aggregate_by(\@rows, 'table_name');
Groups a flat array of row hashrefs into { $key_value = [\%row, ...] }>. Preserves row order within each group. The \@rows arrayref is left unmodified; a row whose key field is undef is skipped.
_aggregate_by_ordered
my $groups = $class->_aggregate_by_ordered(\@rows, 'constraint_name');
for my $pair (@$groups) {
my ($key, $group_rows) = @$pair;
...
}
Like "_aggregate_by", but returns an ArrayRef of [ $key, [\%row, ...] ] pairs in first-seen key order, so callers that need deterministic group ordering (e.g. composite foreign-key columns, which must keep their declared local/remote pairing) do not have to reimplement the grouping. Row order within each group is preserved. Rows whose key field is undef are skipped.
table_keys
Returns an ArrayRef of opaque table-key strings (typically schema.table or just table). These are passed as $key to all other contract methods. Default: the sorted bare table names from the canonical tables section.
table_columns
my \@names = $intro->table_columns($key);
Ordered list of column names for $key. Default: the column_name of each entry in the canonical columns section, in declaration order.
table_columns_info
my $info = $intro->table_columns_info($key);
Hashref { col_name = { data_type, size, is_nullable, default_value, is_auto_increment, ... } }>. Default: built from the canonical columns section -- size and is_auto_increment are included only when present on the column.
is_nullable is 0 when the column carries not_null or when it is part of the primary key (is_pk), and 1 otherwise. not_null answers a different question -- whether the column carries a NOT NULL attribute -- and engines keep the primary-key constraint separate from that attribute, so SQLite's PRAGMA table_info reports notnull = 0 for a key column.
The fold is a statement about DBIO's model, not a claim about what the engine would refuse: is_pk marks the column "table_pk_info" names as the row's identity, and an identity that can be NULL is not usable in the Row layer. A key column is therefore is_nullable => 0 however the model spells its attribute, and the two contract methods cannot disagree about the same column -- both read is_pk off the same hashref. A model that carries no is_pk is unaffected.
That choice has a real cost on an engine that does permit NULL in a key column: the base reports is_nullable => 0 for a column that holds NULLs, DBIO::Generate bakes it into the regenerated result class, and a redeploy emits a stricter column than the one introspected. The escape is a driver override of this method -- which is what DBIO::ClickHouse::Introspect does deliberately, reading nullability out of the type string where ClickHouse actually writes it. Two engines are known to permit it, both only under precise conditions:
ClickHouse, when the table is created with
SETTINGS allow_nullable_key = 1. MergeTree refuses aNullablecolumn in the sorting key only while that setting sits at its default0.SQLite, for any
PRIMARY KEYon a rowid table other than the rowid alias itself. This is SQLite's documented, deliberately-kept bug (see "The PRIMARY KEY" in https://sqlite.org/lang_createtable.html):CREATE TABLE t (name TEXT PRIMARY KEY)acceptsINSERT INTO t (name) VALUES (NULL)repeatedly, and the NULLs do not even collide with one another. SQLite states the exclusions itself -- it allows the NULL "unless the column is an INTEGER PRIMARY KEY or the table is a WITHOUT ROWID table or a STRICT table or the column is declared NOT NULL".The declared type does not identify the alias, and reading the rule as "
INTEGERmeans no NULL" is wrong twice over. Only the exact declared typeINTEGERmakes a rowid alias, soid INT PRIMARY KEYis an ordinary indexed column with integer affinity -- and stores NULL. And a column-levelPRIMARY KEY DESCis documented as not producing an alias either, soid INTEGER PRIMARY KEY DESCstores NULL as well, while the table-constraint spellingPRIMARY KEY(id DESC)does produce one.PRAGMA table_inforeportstype=INTEGER notnull=0 pk=1for the alias and theDESCnon-alias alike.The exact separator is
PRAGMA index_list: SQLite materialises every primary key as an automaticorigin = 'pk'index except the rowid alias, which is the rowid itself and needs no index. On a rowid table, "declares a key but has nopk-origin index" is therefore the alias, with no parsing of the storedCREATE TABLEtext. DBIO::SQLite::Introspect overrides this method on exactly that test.A
WITHOUT ROWIDtable needs no case of its own: SQLite reportsnotnull = 1for its key columns itself, so the fold changes nothing there.
A driver that takes this escape will make "table_pk_info" and is_nullable disagree, and the Row layer says so. That is the intended outcome, not a defect in the override: introspection reports what the engine really stores, DBIO::Generate writes is_nullable => 1 into the generated result class, and "set_primary_key" in DBIO::ResultSource then carps that the key includes a nullable column and that Result-object operations will fail. The warning is correct -- it is the same model statement this fold is made of -- and arriving at load time is the point: the user learns that the foreign table is not usable as a DBIO source when they load it, not when a row cannot be found later.
Deciding this in the model rather than behind a capability flag: ADR 0038.
table_pk_info
my \@pk_cols = @{ $intro->table_pk_info($key) };
Ordered list of primary key column names. Default: the is_pk columns from the canonical columns section, ordered by pk_position (composite keys keep their declared order).
table_uniq_info
my \@constraints = @{ $intro->table_uniq_info($key) };
List of [ $constraint_name, \@col_names ] pairs. Default: the canonical unique_constraints section for $key if that section exists; otherwise derived from the indexes section (unique indexes, skipping primary-key backed ones and any index literally named PRIMARY).
table_fk_info
my \@fks = @{ $intro->table_fk_info($key) };
Each FK is a hashref:
{
local_columns => [qw/author_id/],
remote_table => 'authors',
remote_schema => 'public', # may be undef
remote_columns => [qw/id/], # may be [] (use remote PK)
attrs => {},
}
Default: built from the canonical foreign_keys section. remote_schema is always undef (single-schema), and attrs carries on_delete / on_update.
table_is_view
Returns true if $key is a view rather than a base table. Default: true when the canonical tables entry has kind => 'view'.
view_definition
SQL text of the view definition, or undef.
table_comment
Comment string for the table, or undef.
column_comment
Comment string for a column, or undef.
result_class_extra_statements
my @stmts = $intro->result_class_extra_statements($key);
Optional hook for driver-specific emitter statements. Each element is an arrayref [ $method_name, @args ] which DBIO::Generate emits as __PACKAGE__-method_name(@args)>. The method name is a Perl identifier. A plain string argument is emitted as a string literal; an argument that is Perl code is a scalar reference (\"code"); any other reference is serialised as a literal. Defaults to an empty list.
CONTRACT VERSION
This class exposes an independent compatibility version, distinct from $VERSION (the dist version injected by Dist::Zilla's VersionFromMainModule):
my $v = $class->contract_version;
$CONTRACT_VERSION bumps when the "CANONICAL MODEL" shape or the normalized contract methods ("table_keys", "table_columns", "table_columns_info", "table_pk_info", "table_uniq_info", "table_fk_info", "table_is_view", "view_definition", "table_comment", "column_comment", "result_class_extra_statements") changes. The dist $VERSION bumps on every release, but two core releases at the same contract version remain wire-compatible. Out-of-tree drivers should record the contract version they were last tested against and compare it against core's at load time, warning (or strict-failing under DBIO_STRICT_CONTRACT) when the shapes have drifted. See docs/adr/ for the contract-version policy.
CANONICAL MODEL
The hashref returned by "model" is the substrate every contract method reads. Its shape varies by driver, but there is one canonical shape -- the un-qualified, single-database shape shared by the single-schema drivers (MySQL, Firebird, SQLite). The default contract implementations below are written against it, so any driver whose _build_model produces this shape inherits a working contract for free. Drivers that diverge (PostgreSQL: schema-qualified keys, case normalization, enum/identity/generated columns) override the methods they need.
The canonical model has up to five top-level sections, each a hashref keyed by table name (bare, un-qualified):
tables--{ $table => { table_name => $table, kind => 'table'|'view' } }columns--{ $table => [ \%column, ... ] }in declaration order. Each column:column_name,data_type(bare SQL type),size(undef, a scalar length, or a[precision, scale]pair),not_null(0/1),default_value(orundef),is_pk/pk_position(primary-key membership and 1-based ordinal), and optionallyis_auto_increment.indexes--{ $table => { $index_name => { is_unique, columns => [...], origin, index_type } } }.originis'pk'for a primary-key-backed index (skipped by "table_uniq_info").index_typeis optional -- the engine's index access method / class (BTREE,FULLTEXT,SPATIAL,HASH; PostgreSQLgin/gist/brin/spgist) -- carried only by drivers that introspect and render it. It participates in the canonical index comparison ("changed_index_fields" in DBIO::Diff::Compare) as a bilateral field: a type change is a real diff only when both the live and desired sides carry a type, so a driver that leaves itundef(the default) never produces a phantom index migration for it. See ADR 0052.unique_constraints-- (optional){ $table => [ [ $name, \@cols ], ... ] }. When present, it is the authoritative source for "table_uniq_info"; when absent, unique constraints are derived from theindexessection.foreign_keys--{ $table => [ \%fk, ... ] }. Each FK:from_columns,to_table,to_columns,on_update,on_delete, and optionallyconstraint_name-- the live, server-assigned constraint name, carried by drivers that can introspect it so the diff/deploy layer can prefer the real name for FKDROP/ALTERinstead of a generated one.constraint_nameis FK metadata, not a compared attribute. See ADR 0021.
NORMALIZED CONTRACT
Subclasses that are used as a generation source for DBIO::Generate implement these methods. The first seven ship default implementations that read the "CANONICAL MODEL"; a single-schema driver inherits them unchanged and a divergent driver overrides what it needs. The remaining four return safe defaults.
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.