view release on metacpan or search on metacpan
docs/adr/0011-timestamp-and-helpers-integrated-into-core.md view on Meta::CPAN
Integrate `DBIx::Class::TimeStamp` and the Helper component family directly into
the core distribution, available without a separate install (and, where it makes
sense, without explicit `load_components`).
- **TimeStamp.** `DBIO::Timestamp` (`lib/DBIO/Timestamp.pm`, ABSTRACT at `:2`)
ships in core: `set_on_create` columns auto-populate on insert, `set_on_update`
columns refresh on every update, plus the `col_created` / `col_updated` /
`cols_updated_created` declaration helpers.
- **Helper family.** The Row helpers are folded into `DBIO::Row` (`TO_JSON` /
`serializable_columns`, `self_rs`, `get_storage_value`, the
`before_/after_/around_column_change` callbacks) â these come from
`DBIx::Class::Helper::Row::StorageValues`,
`DBIx::Class::Helper::Row::OnColumnChange`, and
`DBIx::Class::Helper::ResultSet::ProxyResultSetMethod`
(`lib/DBIO/Manual/Heritage.pod:255-257`). Relationship helpers live in
`DBIO::Relationship::Helpers` and its `HasMany`/`HasOne`/`BelongsTo`/
`ManyToMany` components, with many-to-many introspection (formerly
`DBIx::Class::IntrospectableM2M`) now automatic via `_m2m_metadata`.
## Rationale
docs/adr/0025-trace-redact-bind-value-hook.md view on Meta::CPAN
# ADR 0025 â `redact_bind_value()` trace hook for sensitive bind values
- Status: accepted
- Date: 2026-06-25
- Tags: public-api, security, tracing, observability, bind-values
## Context
DBIO's tracing surface â `DBIO::Storage::DBI::_format_for_trace` and the
`trace`/`debug` callbacks it routes through â captures every executed SQL
statement together with its bind values, for live debugging and for the
post-mortem query log drivers and ops teams keep. Bind values are the
authoritative data plane of a request: column values, primary keys, search
predicates, foreign-key targets. They are also where sensitive data lives â
passwords hashed into `WHERE password = ?` predicates, session tokens carried
in `WHERE api_token = ?`, PII in `WHERE ssn = ?`, vault paths in
`WHERE credential_path = ?`.
Until now there was no DBIO-level way to redact a bind value before it lands in
the trace stream. Drivers and consumers could install their own trace callback
docs/adr/0031-resultset-row-real-async.md view on Meta::CPAN
| `select_async` | raw row arrayrefs (cursor `->all` shape) | `_inflate_fetched_rows` â `_construct_results` |
| `select_single_async` | a single raw row arrayref | `single_async` / `count_async` |
| `insert_async($rsrc, \%rowdata)` | the **returned-columns hashref** (autoinc PK + retrieve-on-insert cols), exactly what sync `$storage->insert` returns | `_store_inserted_columns` |
`select_async`/`select_single_async` already match the sync cursor shapes. The
`insert_async` **hashref** shape is the one that needs enforcing: at least
`dbio-postgresql-ev` currently resolves `insert_async` with RETURNING arrayref
rows, which `create_async`/`Row::insert_async` cannot consume. Drivers must align
â tracked as a cross-repo karr ticket (below).
### 4. `then` callbacks may return plain values (auto-wrap)
All RS/Row `then` callbacks return plain values, not Futures. The default family
behaviour (`DBIO::Future::Immediate->then` and the real backends) auto-wraps a plain
return into a resolved Future. `DBIO::Future`'s POD â which said a `then` callback
"must return a new Future" â is corrected to state that a plain value is accepted
and wrapped. This is a binding part of the `DBIO::Future` duck-type: every backend
Future's `then` must auto-wrap plain returns, or these callbacks break on it.
## Consequences
- Storage-level and ResultSet/Row async are now consistent: both croak on a sync
instance, both do real async on a backend, both degrade under `immediate`.
- `DBIO::ResultSet` gains `_rs_run_async`, `_inflate_fetched_rows`,
`_single_select_args`, and the small `_PrefetchedCursor` package; the five RS
`*_async` are rewritten. `DBIO::Row` gains `insert_async`, `_row_run_async`, and
the factored-out `_store_inserted_columns` (sync `insert` unchanged in
behaviour). Mock-tested in `t/resultset/async_backend.t` (a registered mock
lib/DBIO/DeploymentHandler.pm view on Meta::CPAN
DBIO::DeploymentHandler provides schema deployment and version management
using the native DBIO driver deploy system (no SQL::Translator needed).
The driver computes the diff between live database and current code in one
shot, so this handler does not loop over discrete version transitions the
way L<DBIx::Class::DeploymentHandler> does. The DDL part of an upgrade is
always a single reconcile.
For data migrations that the DDL diff cannot express (column backfills,
value normalisation, etc.) C<upgrade_hooks> provides per-version C<pre>
and C<post> callbacks that fire around the DDL apply, in ascending version
order, for every version step that is being crossed. C<pre> hooks see the
old schema, C<post> hooks see the new one.
Schema version is tracked in a C<__VERSION> table.
This is a port of L<DBIx::Class::DeploymentHandler> adapted for DBIO's
native driver architecture.
=head1 TRANSACTIONAL UPGRADE
lib/DBIO/Diff/Op.pm view on Meta::CPAN
=item * L</new> and the C<action> accessor, plus L</mk_diff_accessors> to
declare the rest without hand-rolling C<< sub foo { $_[0]->{foo} } >> lines.
=item * L</diff_toplevel> -- the create/drop walk for a flat keyed collection
(tables: a name exists only in the target -> create, only in the source ->
drop; no in-place change).
=item * L</diff_nested> -- the create/change/drop walk for members nested under
tables (columns, indexes, foreign keys), parameterized by a per-engine
"did this member change?" predicate (e.g. the C<is_same_*> functions from
L<DBIO::Diff::Compare>) and per-engine op-construction callbacks.
=item * L</summary_prefix> -- the C<+>/C<->/C<~> action glyph used in summaries.
=item * L</should_emit_if_exists> -- the F12 helper a per-driver C<as_sql>
calls to decide whether to emit C<IF [NOT] EXISTS> guards.
=back
Subclasses keep C<as_sql> (the real engine seam) and a thin C<summary>, and
call these helpers from their own C<diff> class method. Drivers whose diff is
lib/DBIO/Future.pm view on Meta::CPAN
=over 4
=item then
$future->then(sub { my @result = @_; ... });
Success callback. Called with the resolved values when the Future completes
successfully. May return either a new Future (which is chained and flattened) or
a plain value, which is wrapped into an immediately-resolved Future. A conforming
Future B<must> auto-wrap a plain return -- the storage and ResultSet C<*_async>
C<then> callbacks rely on it (ADR 0031).
=item catch
$future->catch(sub { my $error = shift; ... });
Error callback. Called with the error when the Future fails. Like L</then>, may
return a new Future or a plain value, which is wrapped into a resolved Future.
=item get
lib/DBIO/Manual/Component.pod view on Meta::CPAN
These ship in the DBIO core distribution and are loaded explicitly via
C<load_components>.
L<DBIO::InflateColumn::DateTime> -- Inflate date/time columns to
L<DateTime> objects.
L<DBIO::InflateColumn::Serializer> -- Serialise complex Perl structures
into a single column (JSON, Storable, YAML).
L<DBIO::FilterColumn> -- Bidirectional filter callbacks between in-memory
and storage representations.
L<DBIO::Timestamp> -- C<set_on_create> / C<set_on_update> column flags
for automatic timestamp maintenance.
L<DBIO::EncodedColumn> -- One-way encode columns such as passwords.
L<DBIO::UUIDColumns> -- C<uuid_on_create> column flag for automatic
UUID generation on insert.
lib/DBIO/Row.pm view on Meta::CPAN
my ($self, $upd) = @_;
# ProxyResultSetMethod (integrated helper): strip proxy slots from the
# dirty-column set so UPDATE statements never include them. "delete
# local" localises the deletion for the scope of this sub, so the
# original dirty flags are restored when update returns.
$self->{_dirty_columns} ||= {};
delete local @{$self->{_dirty_columns}}{@{$self->_proxy_slots||[]}};
# OnColumnChange (integrated helper): short-circuit when nothing is
# registered. When callbacks exist, build the before/around/after
# chain, then delegate to _update_body via around-wrapper; the whole
# orchestration is a no-op if none of the registered columns are dirty.
my $has_change_cbs =
$self->_before_change
|| $self->_around_change
|| $self->_after_change;
if ($has_change_cbs) {
$self->set_inflated_columns($upd) if $upd;
lib/DBIO/Row.pm view on Meta::CPAN
$guard->commit if $txn_wrap;
return $ret;
}
return $self->_update_body($upd);
}
# Core update logic, extracted so OnColumnChange (above) can wrap it
# with before/around/after callbacks without mutual recursion.
sub _update_body {
my ($self, $upd) = @_;
$self->set_inflated_columns($upd) if $upd;
my %to_update = $self->get_dirty_columns
or return $self;
$self->throw_exception( "Not in database" ) unless $self->in_storage;
lib/DBIO/Row.pm view on Meta::CPAN
});
$row->title('New');
$row->get_storage_value('title'); # the old value
$row->update;
$row->get_storage_value('title'); # now 'New'
=head2 OnColumnChange
Register C<before_column_change>, C<after_column_change>, and
C<around_column_change> callbacks on a Result class. Callbacks fire
from L</update> only if the named column is actually dirty. The "old"
value comes from L</get_storage_value>, so pair this with
C<< keep_storage_value => 1 >> on the column if you need a real
previous value; otherwise it will be C<undef>.
__PACKAGE__->before_column_change(
amount => { method => 'bank_transfer', txn_wrap => 1 },
);
Callback signatures:
before: $self->$method($old, $new)
after: $self->$method($old, $new) # $old may now equal $new
around: $self->$method($next, $old, $new)
C<before> callbacks fire in definition order, C<after> callbacks fire
in reverse order, C<around> callbacks wrap in definition order (the
innermost being the first declared). If any registered arg has
C<< txn_wrap => 1 >> the whole update is wrapped in a
C<txn_scope_guard>.
See L<DBIx::Class::Helper::Row::OnColumnChange/on_column_change_allow_override_args>
for C<on_column_change_allow_override_args> semantics.
=head2 ProxyResultSetMethod
Expose a C<with_foo> ResultSet method as a row accessor with a
lib/DBIO/Storage/PoolBase.pm view on Meta::CPAN
|| $self->future_class->done($conn);
}
=head2 _register_connection_ready
$self->_register_connection_ready($conn, $ready_future);
Bookkeeping primitive for the L</_connection_ready_future> pattern. Stores a
per-connection readiness Future keyed by C<Scalar::Util::refaddr($conn)>, so
an async L</_create_connection> can hand the Future's C<done> / C<fail> to
the transport's C<on_connect> / C<on_error> callbacks and have
L</_connection_ready_future> find it again by connection. Returns the Future.
=head2 _connection_ready_lookup
my $ready = $self->_connection_ready_lookup($conn);
Return the readiness Future registered for C<$conn> via
L</_register_connection_ready>, or undef if none (e.g. a synchronous pool
that never registered one). An async L</_connection_ready_future> override
typically returns
t/helpers/row_helpers_storage.t view on Meta::CPAN
subtest 'OnColumnChange: before/after fire with correct args' => sub {
my $schema = connect_fake('TestOCC::Schema');
my $rs = $schema->resultset('Account');
# Create and insert an account
TestOCC::Changes::reset();
my $acc = $rs->new_result({ amount => 100, owner => 'alice' });
$acc->insert;
# No callbacks should fire on insert
is(scalar TestOCC::Changes::all(), 0,
'callbacks do not fire on insert');
# Dirty the tracked column and update
$acc->amount(250);
$acc->update;
my @log = TestOCC::Changes::all();
is(scalar @log, 2, 'before + after callbacks fired');
is($log[0]{phase}, 'before', 'before fires first');
is($log[0]{old}, 100, 'before sees old storage value');
is($log[0]{new}, 250, 'before sees new value');
is($log[1]{phase}, 'after', 'after fires last');
# After callbacks get ($old, $new) where $old has been stored â
# the post-update storage value matches the new value.
is($log[1]{old}, 250, 'after sees updated storage value');
is($log[1]{new}, 250, 'after sees new value');
# Updating a non-registered column is a no-op for callbacks
TestOCC::Changes::reset();
$acc->owner('bob');
$acc->update;
is(scalar TestOCC::Changes::all(), 0,
'non-registered column change does not trigger callbacks');
# Confirm zero-cost for a class with no callbacks (the SV schema).
my $post_schema = connect_fake('TestSV::Schema');
my $post_rs = $post_schema->resultset('Post');
my $post = $post_rs->new_result({ title => 'x', body => 'y' });
$post->insert;
$post->title('z');
lives_ok { $post->update } 'update on a class with no callbacks works';
};
subtest 'ProxyResultSetMethod: installs accessor, registers slot' => sub {
my $schema = connect_fake('TestPRM::Schema');
# Class method is installed as an accessor sub on the Result class
can_ok(
'TestPRM::Schema::Result::Widget',
'score',
);