DBIO

 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',
  );



( run in 2.336 seconds using v1.01-cache-2.11-cpan-6aa56a78535 )