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 a Nullable column in the sorting key only while that setting sits at its default 0.

  • SQLite, for any PRIMARY KEY on 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) accepts INSERT 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 "INTEGER means no NULL" is wrong twice over. Only the exact declared type INTEGER makes a rowid alias, so id INT PRIMARY KEY is an ordinary indexed column with integer affinity -- and stores NULL. And a column-level PRIMARY KEY DESC is documented as not producing an alias either, so id INTEGER PRIMARY KEY DESC stores NULL as well, while the table-constraint spelling PRIMARY KEY(id DESC) does produce one. PRAGMA table_info reports type=INTEGER notnull=0 pk=1 for the alias and the DESC non-alias alike.

    The exact separator is PRAGMA index_list: SQLite materialises every primary key as an automatic origin = 'pk' index except the rowid alias, which is the rowid itself and needs no index. On a rowid table, "declares a key but has no pk-origin index" is therefore the alias, with no parsing of the stored CREATE TABLE text. DBIO::SQLite::Introspect overrides this method on exactly that test.

    A WITHOUT ROWID table needs no case of its own: SQLite reports notnull = 1 for 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 (or undef), is_pk / pk_position (primary-key membership and 1-based ordinal), and optionally is_auto_increment.

  • indexes -- { $table => { $index_name => { is_unique, columns => [...], origin, index_type } } }. origin is 'pk' for a primary-key-backed index (skipped by "table_uniq_info"). index_type is optional -- the engine's index access method / class (BTREE, FULLTEXT, SPATIAL, HASH; PostgreSQL gin/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 it undef (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 the indexes section.

  • foreign_keys -- { $table => [ \%fk, ... ] }. Each FK: from_columns, to_table, to_columns, on_update, on_delete, and optionally constraint_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 FK DROP/ALTER instead of a generated one. constraint_name is 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.