NAME
DBIO::Storage::DBI::Capabilities - Three-tier capability probing for DBI storage drivers
VERSION
version 0.900004
DESCRIPTION
Capability detection for DBIO::Storage::DBI. Every capability X is answered by asking three tiers in order and taking the first defined answer. The tiers are separate slots, and which one a value lands in is decided by how it was written, not by which accessor was used:
- 1. The per-storage force --
$storage->_use_X(0|1) -
Written on an instance. This is the caller's slot: one application, one storage object, overriding whatever the driver believes. It lives in the storage's own hash and dies with it.
Clear it with
$storage->_use_X(undef), which hands the question back to tier 2 (see "Clearing a force" --undefremoves the force, it does not mean "off"; "off" is0). - 2. The class-level declaration --
__PACKAGE__->_use_X(0|1) -
Written on a class, in a driver's storage class body. This is the driver's slot for a fact that is a static property of the driver. It lives in a package variable and is inherited down
@ISA, so a subclass of the declaring storage gets it too.It is not the same slot as tier 1 --
set_inheritedwrites the instance hash for an instance invocant and the$Class::__cag_*package variable for a class one -- which is why a caller can force over a class declaration and get the declaration back afterwards. - 3. The determination --
sub _determine_supports_X -
The driver's slot for a fact that has to be probed: an answer that depends on the server version, on which
DBDis loaded, on anything only the live$dbhknows. Called through_supports_X, a simple-style accessor that caches the answer in a slot wiped on every$dbhreconnection (a reconnect is not guaranteed to land on the same RDBMS version)._determine_supports_Xdoes not need to exist on a driver -- the runtime->can-checks for it before calling, and a capability with neither a declaration nor a determiner reads as0.
$storage->_use_X is the accessor to read: it is the only one that consults all three tiers, and it is what every consumer in core asks ("upsert" in DBIO::ResultSet, "results_exist" in DBIO::ResultSet, "should_emit_if_exists" in DBIO::Diff::Op, "_execute_ddl" in DBIO::Deploy::Base, DBIO::DeploymentHandler). $storage->_supports_X is tier 3's own accessor and answers for tier 3 alone -- on a storage whose driver declared in tier 2 it will report the default while _use_X reports the declaration, and that is not a disagreement to fix but the difference between "what did the probe find" and "what is the answer".
Which tier a driver writes to
Tiers 2 and 3 are both the driver's. Pick by what is being declared, not by what the caller might want to do with it -- the caller's slot is tier 1 and neither declaration form takes it away:
A static fact about this driver -- "this storage renders the standard
CASE WHEN EXISTSform", "this engine commits DDL implicitly" -- goes in tier 2,__PACKAGE__->_use_X(0|1). It answers as a class method, before any connection and before any storage object exists.A probed fact -- "PostgreSQL 9.5 and up", "MariaDB 10.5 and up" -- goes in tier 3,
sub _determine_supports_X, where it can read_server_info.
Declaring in both tiers with the same constant is harmless but says nothing extra; one is enough.
Clearing a force
$storage->_use_X(undef) removes the tier-1 force, and the question falls to tier 2 and then tier 3 -- so a caller who forces a capability off and later changes their mind gets the driver's answer back, not core's default.
This is not what Class::Accessor::Grouped does on its own, and the difference is the reason set_use_dbms_capability exists as an override rather than a straight call to set_inherited: get_inherited reads an instance slot with exists, not defined, so a plain undef assignment would leave a tombstone shadowing the class declaration for the life of the storage instead of clearing anything. Assigning undef to an instance therefore deletes the slot. On a class invocant nothing special happens -- get_inherited already tests defined when it walks classes, so an undef declaration falls through by itself.
Driver-declared capabilities and the per-name accessor
A driver may add capabilities of its own:
__PACKAGE__->mk_group_accessors( dbms_capability => '_supports_X' );
__PACKAGE__->mk_group_accessors( use_dbms_capability => '_use_X' );
The three tiers then work exactly as above -- but the two per-name accessors are installed in the class that declared them, and core reblesses a storage into its driver class lazily, on first use. On a freshly connected storage that has not been reblessed yet, $storage->_use_X(...) is therefore not a method at all:
Can't locate object method "_use_X" via package "DBIO::Storage::DBI"
Core cannot install it in advance: it does not know the name, and the driver's storage class is not even loaded until determination runs. Two forms work regardless, and a driver's POD should name one of them:
DBIO::Yourdb::Storage->_use_X(0|1)-- the class form. Valid from the moment the storage class is loaded, needs no storage object, and is tier 2, so it is the right form for a deployment-wide setting.$storage->set_use_dbms_capability('_use_X', 0|1)and$storage->get_use_dbms_capability('_use_X')-- the two-argument form underneath every_use_accessor. Name-independent, so it works before the rebless: the getter is onDBIO::Storage::DBI's driver-determination list and reblesses first, the setter writes the tier-1 slot the driver's accessor reads afterwards.undefclears there too.
Both accessors want a storage instance for anything they have to determine. A class-method call only ever succeeds where the value was declared outright in tier 2, because everything else has to be determined from the connected $dbh. Asking a class for an undeclared capability therefore throws, naming the capability.
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 capability-probing interface (the _supports_* / _use_* accessors, the default capability list, or the contract surface that drivers extend with new entries) 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.
Version history of this class: 1.3 adds _adjust_bind_values, the one seam for adjusting values before they are bound, on both bind paths.
CAPABILITY LIST
The default capability set is the following. Drivers add their own with __PACKAGE__->mk_group_accessors(dbms_capability => '_supports_X').
insert_returning/insert_returning_bound-
INSERT ... RETURNING support.
multicolumn_in-
Multi-column IN list optimisation.
placeholders/typeless_placeholders-
Driver placeholder style.
join_optimizer-
Join-order hinting. On by default; not strictly a capability.
transactional_ddl-- F02 / F10-
True iff the engine runs DDL inside a normal transaction (i.e.
txn_do { do_ddl() }is atomic). False if the engine forces an implicit COMMIT on DDL (MySQL, Oracle, Sybase, Informix), depends onAutoCommit=onfor in-place rebuilds (SQLite), or is capable but deliberately declines for driver-specific reasons (DB2). DBIO::Deploy::Base uses this to decide whether to wrap a multi-statement DDL loop in$storage->txn_do; DBIO::DeploymentHandler uses it to decide whether the whole upgrade can run atomically. Drivers register their engine value via__PACKAGE__->_use_transactional_ddl(0|1)(in the driver's storage class) or by defining_determine_supports_transactional_ddl.Its per-statement companion is the
_ddl_statement_is_transactionalhook below: the capability answers for the connection, the hook for the odd statement that cannot be in a transaction anyway. supports_if_exists-- F12-
True iff the engine / diff emitter can use
IF [NOT] EXISTSin DDL (CREATE TABLE IF NOT EXISTS,DROP TABLE IF EXISTS,ALTER TABLE ... ADD COLUMN IF NOT EXISTS). False on engines that do not parse the syntax (older MySQL / MariaDB before 10.0.2, some SQLite builds, others). DBIO::Diff::Op::should_emit_if_exists probes this to decide whether to emit guarded DDL. Drivers register their engine value via__PACKAGE__->_use_supports_if_exists(0|1)or by defining_determine_supports_supports_if_exists. supports_upsert-- ADR 0036-
True iff the engine has a single-statement, server-side atomic upsert that rides on
INSERT(PostgreSQL / SQLiteON CONFLICT ... DO UPDATE|NOTHING, MySQLON DUPLICATE KEY UPDATE) and the driver's DBIO::SQLMaker subclass implements theon_conflictoption ofsql_maker->insert. False everywhere by default -- including on engines whose upsert is aMERGEstatement (Oracle, DB2, SQL Server), which is a different statement shape and not covered by this capability."upsert" in DBIO::ResultSet probes this and throws when it is false; it never degrades silently to a client-side read-then-write. Drivers register their engine value via
__PACKAGE__->_use_supports_upsert(0|1)or by defining_determine_supports_supports_upsert(version-gated, as PostgreSQL does forinsert_returning).MERGE-based upsert is a deliberate, permanent non-goal -- not an oversight. The exclusion of the
MERGEengines (Oracle, DB2, SQL Server) above is a decision, not an accident of the default0: they stay without$rs->upsertunder this capability for good, becauseMERGEis a different feature rather than a driver nobody has written yet. Two structural facts say so:Two correlation names, one vocabulary. This API's condition vocabulary ("upsert" in DBIO::ResultSet's
update_where) names the stored row with a bare column and the attempted row with{ -attempted => $col }, and that is unambiguous only because anINSERT ... ON CONFLICThas exactly one target correlation plus the oneEXCLUDEDpseudo-row. AMERGEhas two named sides -- source and target -- so the stored side comes out unqualified and ambiguous. Covering it would need a second, symmetric qualifier this vocabulary does not have.The identity guarantee does not carry. ADR 0036's rule that a driver may declare
supports_upsertonly if it can prove which row it wrote rests, for the INSERT-borne engines, on an insert having happened (RETURNING, or the engine's insert-identity channel). When aMERGEtakes itsUPDATEbranch there was no insert and no generated key -- verbatim the SQLite-below-3.35 failure mode ADR 0036 rejects as "silent bad data, worse thanundef". The guarantee must be re-derived forMERGE, not inherited.
A future
MERGEupsert therefore needs its own capability (supports_merge_upsertor the like) with guarantees reasoned from scratch -- never a widening of this flag or a second alias bolted onto this vocabulary. See ADR 0036 for the full rationale. supports_upsert_where-- ADR 0036-
True iff the driver renders the conditional conflict branch --
DO UPDATE SET ... WHERE ..., thewherekey of theon_conflictstructure, reachable as "upsert" in DBIO::ResultSet'supdate_whereoption. PostgreSQL and SQLite have the grammar; MySQL has no equivalent (ON DUPLICATE KEY UPDATEtakes noWHERE, and a per-columnIF()does not suppress the write). Implies nothing on its own: it is only ever consulted together withsupports_upsert.This is the one dialect difference that gets a flag of its own, and the reason is that it is the only one a driver can ignore silently. Dropping the conflict target emits invalid SQL on PostgreSQL, ignoring
action => 'nothing'leaves no set to write -- those announce themselves. Dropping theWHEREproduces a perfectly valid unconditionalUPDATE, i.e. the last-write-wins overwrite the condition was written to prevent. Default0therefore keeps every driver that has never heard of the option correct, and core throws instead of handing it one.Answer about the branch itself; never derive the answer from
supports_upsert. The two flags earn their keep by answering two different questions, so a flag that always repeats the other one carries no information at all -- and deriving it also imports whatever the other gate happens to require for its own reasons. PostgreSQL'ssupports_upsert, for one, additionally demandsINSERT ... RETURNING, because that is what proves the row handed back is the row that was written; a condition that does not hold writes no row by design, so folding that requirement in here would make this flag report a limitation the conditional branch does not have. Nor is a second0any help diagnostically: core probessupports_upsertfirst and throws there, so a driver gated off for an unrelated reason is already named by the right gate, with that gate's message.The legitimate uses of
_determine_supports_supports_upsert_whereare the ones that still measure the branch: an engine whose own support for the conditional clause is version-dependent, which is the same fact, probed. What is not an input to this answer is anything the branch does not depend on --RETURNING, the identity transport, connection state. Drivers register their engine value via__PACKAGE__->_use_supports_upsert_where(0|1)where it is a static fact, or as a_determine_supports_supports_upsert_wherewhere it has to be probed; both leave the_use_force to the caller, because the class-level declaration and the per-storage force are different slots ("Clearing a force"). For PostgreSQL and SQLite the rule yields an unconditional yes:ON CONFLICTarrived complete on either engine (PostgreSQL 9.5, libsqlite 3.24),WHEREincluded, so neither has a version that speaks the conflict grammar but not the condition. supports_exists_query-- ADR 0037-
True iff the storage can render a boolean-valued
EXISTSquery this engine accepts -- either because the base rendering of "_exists_value_sql" in DBIO::Storage::DBI is right for it (SELECT CASE WHEN EXISTS <inner> THEN 1 ELSE 0 END, correct on PostgreSQL, MySQL/MariaDB, SQLite, DuckDB, SQL Server and Sybase), or because the driver overrode that hook with the spelling its engine needs (OracleFROM DUAL, DB2FROM SYSIBM.SYSDUMMY1, FirebirdFROM RDB$DATABASE)."results_exist" in DBIO::ResultSet, "results_exist_async" in DBIO::ResultSet and "results_exist_as_query" in DBIO::ResultSet probe this and throw when it is false; they never degrade silently to "has_rows" in DBIO::ResultSet, which is the portable alternative the exception names. False everywhere by default: the flag is not vacuous for a driver that does not override the hook, because it asserts that somebody checked -- and the engines nobody has checked are exactly the ones a default-on would ship broken SQL to. Drivers register their engine value via
__PACKAGE__->_use_supports_exists_query(0|1)or by defining_determine_supports_supports_exists_query.
PER-STATEMENT DDL SAFETY
_ddl_statement_is_transactional
sub _ddl_statement_is_transactional {
my ($self, $stmt) = @_;
return 0 if $stmt =~ /^\s*ALTER \s+ TYPE \b .* \b ADD \s+ VALUE \b/xi;
return 1;
}
Asked once per DDL statement by "_execute_ddl" in DBIO::Deploy::Base, but only on a storage that already reports transactional_ddl. Answer false for a statement this engine refuses to run inside a transaction, or runs but leaves unusable until commit.
_execute_ddl groups consecutive statements by this answer and keeps the original order: each run of transaction-safe statements gets one $storage->txn_do, and a statement that answered false runs on its own, outside any transaction. A batch where everything answers true produces a single run and is therefore identical to bracketing the whole loop, which is why a driver that never defines this hook sees no change at all.
The default is true, and overriding it is a deliberate act with a cost: the batch is no longer one transaction, so a failure does not take the whole upgrade back, and core announces the split once. Only define it where the engine leaves no choice. The known case is PostgreSQL's ALTER TYPE ... ADD VALUE, rejected outright in a transaction block before server 12 and, from 12 on, accepted while the new value stays unusable until that transaction commits.
Match on the statement text with care — it is the DDL your own driver emits, not arbitrary user SQL, so anchoring on the forms your DDL and diff classes actually produce is both sufficient and safer than a broad pattern.
DDL SPLITTING
_ddl_sql_dialect
sub _ddl_sql_dialect { 'postgresql' }
Names the SQL dialect the engine's DDL is written in, for the one reader that must know it: the splitter "_split_statements" in DBIO::SQL::Util that "_execute_ddl" in DBIO::Deploy::Base (and, for the async deploy, DBIO::Storage::Async) cuts install and upgrade DDL with. The text of a statement does not say which dialect it is in, and one place -- the backslash in a string literal -- reads differently: a character in standard SQL (PostgreSQL, SQLite), an escape in MySQL and MariaDB. Left to guess, the splitter takes both readings, which cuts a literal that ends on a backslash in front of a later literal with a semicolon. The engine knows; this hook is how it says so.
Allowed values: postgresql, sqlite, mysql (MariaDB declares mysql), oracle (a PL/SQL unit is one statement that keeps the semicolon behind its END). The default is undef, "not declared", which leaves the splitter exactly as it was: both readings. An unknown name makes the splitter throw, so a typo is loud instead of silently reading the wrong way.
It is a fact about the engine like transactional_ddl and "_ddl_statement_is_transactional", which is why it lives on the storage and not on a deploy class: the sync deploy reaches it through $schema->storage, the async deploy through its owner storage, and both end in the same answer. Define it once on the driver's base storage class.
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.