NAME
DBIO::Firebird::Deploy - Deploy and upgrade Firebird schemas via test-deploy-and-compare
VERSION
version 0.900004
DESCRIPTION
DBIO::Firebird::Deploy orchestrates the deployment and upgrade of Firebird schemas using a test-deploy-and-compare strategy. The orchestration (install, diff, apply, upgrade, and the create-temp / deploy-introspect / drop-temp flow) is inherited from DBIO::Deploy::Base::TempDatabase; this class supplies only the Firebird-specific seams:
the three class-name hooks ("_ddl_class" in DBIO::Deploy::Base,
_introspect_class,_diff_class);_create_temp_db/_drop_temp_db-- Firebird creates the temp database out-of-band viaDBD::Firebird->create_databaseand drops it via$dbh->func("ib_drop_database"), because DSQL cannot prepare database-level DDL (do("CREATE DATABASE")/do("DROP DATABASE")fail);_temp_dsn-- Firebird'sdbi:Firebird:dbname=$idDSN form, where$idis the fullhost/port:/abs/path.fdbidentifier returned by_create_temp_db.
my $deploy = DBIO::Firebird::Deploy->new(
schema => MyApp::DB->connect($dsn),
);
$deploy->install; # fresh install
$deploy->upgrade; # test-deploy + compare + apply
my $diff = $deploy->diff; # or in steps
print $diff->summary;
$deploy->apply($diff) if $diff->has_changes;
Errors do not quote the password
This class creates the temporary database, connects to it once more to drop it, and reads the live DSN to find out where to put it. What fails there fails with a text that is not this module's: DBI dies quoting the data source it was given, create_database croaks with whatever the engine makes of a CREATE DATABASE statement that carries the password in its text, and the DSN is the caller's. Each of these messages goes through core's DBIO::Util::mask_connect_secrets together with the connect info it is about: the password is replaced by *** wherever the text repeats it, and so is the value of a password= part of a DSN. The database, the host and the user name stay -- the message still says which connection failed.
A schema on an AccessBroker
With an DBIO::AccessBroker the storage's connect_info is [ $broker ] and holds nothing of the broker's answer. Deploy reads the answer of the moment from the storage (_dbi_connect_info, normalised to DSN, user and password) to find the live database's server and directory and to connect to the temp database, so diff, upgrade and install work as with a plain DSN. A broker whose answer has a coderef for the DSN (or a dbh_maker) is refused as a coderef DSN is, with a message that carries no DSN and no password.
Names that differ only in case -- refused
A database whose objects were created unquoted (by hand or with another tool) holds ARTIST where the schema says artist. Diffed name by name that is a table to drop and a table to create, and upgrade would empty it. It does not: upgrade and apply die before they send a statement, and the object diff returns dies on first use (has_changes, as_sql, summary), with an error that lists every such table, column and index. What it says, and the ways out: "NAMES THAT DIFFER ONLY IN CASE" in DBIO::Firebird::Diff.
Transactional DDL -- not claimed
Firebird has transactional DDL and this deploy still does not use it. DBIO::Firebird::Storage::Common declares _use_transactional_ddl(0), so "_execute_ddl" in DBIO::Deploy::Base runs the statements one at a time and core emits its one-time "non-transactional DDL" notice. The 0 is a decision, not a report about the engine: metadata in Firebird lives in ordinary MVCC records in the rdb$ system tables, a DDL statement stays in the user transaction until it ends, and a ROLLBACK takes it back -- which is exactly why isql carries SET AUTODDL (default ON, committing each DDL statement as it runs; isql -n hands DDL back to the transaction).
What is withheld is narrower and is the only thing _execute_ddl acts on: that this driver's whole batch may be bracketed in one $storage->txn_do and come out atomic. Three things were thought to stand in the way; the first and the third were measured on Firebird 5.0.4 (see Measured below), the value stays 0.
- 1. The diff contains a same-name
DROP/CREATEpair. -
DBIO::Firebird::Diff::Index renders a changed index as
DROP INDEX "n"followed byCREATE INDEX "n"-- one name, two adjacent statements. Today a realCOMMITseparates them. Inside one transaction the re-create stores anrdb$indicesrow whose name duplicates one this transaction has deleted without committing, and Firebird polices that name through its own unique indexrdb$index_5, whose violation is spelled attempt to store duplicate value (visible to active transactions).This one decides the question rather than merely raising it, because it is a constraint on a pair. The per-statement companion of the capability,
_ddl_statement_is_transactional, can say "this statement may not be inside a transaction"; it cannot say "these two may not be inside the same one". Answering false forDROP INDEXwould break the pair apart by accident, while asserting something untrue about the statement and taking every index drop out of the atomic batch anyway. - 2.
CREATE INDEXdoes its work atCOMMIT. -
Firebird 5.0 Language Reference,
ALTER INDEX: "if ourALTER INDEXsucceeds and starts to rebuild the index atCOMMIT, other transactions modifying that table will fail or wait, according to theirWAIT/NO WAITattributes. The situation is the same forCREATE INDEX." (Same wording in the 2.5 reference, so it is not version-bounded.) Under a bracket every index build in the batch lands on the single commit at the end: a failingCREATE UNIQUE INDEXis then reported by the commit rather than by the statement that caused it, and concurrent writers are held against for the length of the whole deploy instead of one statement. - 3.
ib_softcommitmakes both the commit and the rollback retaining. -
DBIO::Firebird::Storage::InterBase tells you to switch it on -- without it a commit closes the cursors still open on the connection -- and it makes DBD::Firebird use
isc_commit_retainingandisc_rollback_retaining. Per the Firebird 5.0 Language Reference,RETAINmeans the transaction "is restarted transparently with the same Transaction ID" and, on the rollback side, that "the transaction context is to be retained". Nothing depends on that today, because nothing ever rolls DDL back. A1would make recovery depend on a retaining rollback undoing a batch of metadata changes, which no documentation covers. Jaybird, the official Firebird JDBC driver, declines to use commit retaining during normal execution at all.
The notice core prints is accurate: an apply that fails half way leaves the earlier statements applied. Re-running upgrade is safe, because the diff is recomputed against live state and the second run picks up only what is left.
Either way only the live connection was ever in question. diff builds its target model by deploying into a handle DBIO::Deploy::Base::TempDatabase opened itself, and _execute_ddl never brackets a handle the storage does not own -- so that half runs unwrapped and silent whatever this capability says, and the throwaway database is dropped whole on failure.
Both questions were measured on Firebird 5.0.4: DROP INDEX "n" plus CREATE INDEX "n" survives one transaction, and a txn_do rollback under ib_softcommit takes a batch of rdb$ changes back. The value nevertheless stays 0 (maintainer's decision); the second point still holds. Should the pair ever fail on another version, the fix belongs in DBIO::Firebird::Diff::Index -- rename through a distinct index name -- not in the per-statement hook, which cannot express the constraint.
Measured (Firebird 5.0.4, DBD::Firebird 1.39, a table with an index i on column a; Firebird 3 and 4 were not available and are not covered; the value stays 0, flipping it is the maintainer's decision). Through $storage->txn_do, each time with ib_softcommit off and on, with the same result:
DROP INDEX i,CREATE INDEX ion columnband aCREATE TABLEin onetxn_dothat dies: the index is back on columnaand the table does not exist. The same three statements in atxn_dothat returns: the index is onband the table exists. So point 1 above does not occur on 5.0.4 -- the same name is stored again in the transaction that deleted it -- and a retaining rollback does take therdb$changes back.Point 2 holds:
CREATE UNIQUE INDEXover duplicate values succeeds as a statement and fails atCOMMIT, with the server's attempt to store duplicate value ... in unique index error. Throughtxn_doDBI itself reports the failed commit as DBD driver has not implemented the AutoCommit attribute, the server's text being only in$dbh->errstr; DBIO::Firebird::Storage::InterBase raises the server's text instead, as a DBIO::Exception::Server (DBI Exception: ..., as a refused statement is). The index does not exist afterwards and the connection goes on working.
Not measured: Firebird 3 and 4, and anything with another transaction writing to the table at the time.
ATTRIBUTES
schema
A connected DBIO::Schema instance using the DBIO::Firebird component. Required. (Inherited from DBIO::Deploy::Base.)
METHODS
_check_before_apply
$self->_check_before_apply($diff);
The driver's checks of "apply" in DBIO::Deploy::Base (core ADR 0080), run for a released diff before its first statement. apply and upgrade are DBIO::Deploy::Base's as they are: the guard for destructive and lossy operations (allow_destructive; "lossy" in DBIO::Firebird::Diff::Column says which column changes need it), dry_run, and the statements run through its _execute_diff.
The hook renders the whole diff first (a refused column change or table-view change dies here, and a refusal of the diff itself comes before one that needs the rows), then looks at the rows of the live tables with "_refuse_unfillable". dry_run checks nothing and asks the server nothing.
_refuse_unfillable
$self->_refuse_unfillable($diff, $dbh);
Looks at the rows of the live tables before the first statement runs and refuses -- a DBIO::Exception naming table and column, every such column in one message -- what the Firebird server would answer half way through the diff (measured on Firebird 5.0.4):
a
NOT NULLcolumn without a default (or withDEFAULT NULL) that the diff adds to a table that holds rows: the server has no value for the rows already there. A column with a default is not refused (the server fills the rows with it), nor an identity column (its own rule, "COLUMN CHANGES THE SERVER REFUSES" in DBIO::Firebird::Diff::Column);a column that becomes
NOT NULLin a table that has aNULLin it (Cannot make field NOT NULL because there are NULLs present).
An empty table, and a table with no NULL in the column, pass: nothing is refused that the server takes. The ops before the failing one in the diff are applied by then and an upgrade would stand half done, which is why the rows are asked first.
SEE ALSO
DBIO::Deploy::Base::TempDatabase - shared temp-database orchestration
DBIO::Firebird - schema component
DBIO::Firebird::DDL - generates the DDL used by
installanddiffDBIO::Firebird::Introspect - reads the live and temp database state
DBIO::Firebird::Diff - compares the two introspected models
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.