DBIO
view release on metacpan or search on metacpan
share/skills/dbio-core/SKILL.md view on Meta::CPAN
name => { data_type => 'varchar', size => 255 },
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(posts => 'MyApp::DB::Result::Post', 'user_id');
```
Relationships: `belongs_to`, `has_many`, `has_one`, `might_have`, `many_to_many`.
ResultSet chaining: `$schema->resultset('User')->search({active=>1})->search({role=>'admin'})->order_by('name')`
## Testing Rules
- Core tests MUST use `DBIO::Test::Storage` (fake). Never `dbi:SQLite` or real DB in core
- Driver integration: `DBIO_TEST_PG_DSN`, `DBIO_TEST_MYSQL_DSN`, etc.
- `t/` = tests; `xt/` = author tests
- Shared test schemas â `DBIO::Test::Schema::*` in `dbio/lib/` (see Shared Schemas below for the OO-variant siblings). Do NOT redefine result classes inline in driver tests
- Do NOT nest a new standalone demo schema under `DBIO::Test::Schema::*` â that namespace is walked by a no-arg `load_classes` sweep (t/102load_classes.t); a class there with its own optional deps (Moose/Moo/...) dies the sweep fatally on boxes lac...
- Optional dep skip:
```perl
BEGIN { eval { require Moo; 1 } or plan skip_all => 'Moo not installed' }
```
List in cpanfile as `suggests`, never `requires`
- **Mock storage cursor invariant (karr #55)**: the fake cursor's
captured SQL must equal the SQL a real cursor would have executed.
Any new test infra on the fake storage must route through
`$storage->_select_args` to apply join pruning and complex-prefetch
rewriting. The diff between fake-captured and as_query is a bug.
## Shared Schemas
| Schema | Layer | DDL |
|--------|-------|-----|
| `DBIO::Test::MooSchema` | Moo | `add_columns` |
| `DBIO::Test::MooseSchema` | Moose | `add_columns` |
| `DBIO::Test::MooCakeSchema` | Moo + Cake | Cake DDL |
| `DBIO::Test::MooseSugarSchema` | Moose + Cake | Cake DDL |
Each: Artist + CD, has_many/belongs_to, one custom + one default ResultSet.
## AccessBroker
AccessBroker provides connection credential management and routing. Pass a broker to `Schema->connect($broker)` instead of raw DSN.
```perl
use DBIO::AccessBroker::Static;
my $broker = DBIO::AccessBroker::Static->new(
host => 'localhost',
dbname => 'myapp',
user => 'myapp',
password => 'secret',
);
my $schema = MyApp::Schema->connect($broker);
```
### Interface
All brokers must implement:
A broker is a **CredentialSource**: one backend identity, one set of credentials. It does NOT route and does NOT own a host list â routing + topology belong to `DBIO::Replicated`. See `CONTEXT.md`.
| Method | Returns | Purpose |
|--------|---------|---------|
| `connect_info_for` | HASHREF | `{host, port, dbname, user, password, dbi_attrs}` |
| `connect_info_for_storage($storage)` | HASHREF | Storage-aware version |
| `needs_refresh` | Bool | True if credentials need rotation |
| `refresh` | - | Perform credential rotation |
| `has_rotating_credentials` | Bool | True if credentials rotate |
| `is_transaction_safe` | Bool | False if rotating (default) |
| `for_host($host)` | broker view | One credential, pinned to one host (HostBound) |
The trailing `$mode` ('read'/'write') arg is **vestigial** â accepted for back-compat, ignored. Routing decides read vs write, not the broker.
### Implemented Brokers
| Broker | File | Use Case |
|--------|------|----------|
| `DBIO::AccessBroker::Static` | Static.pm | Single DSN, transaction-safe |
| `DBIO::AccessBroker::Vault` | Vault.pm | TTL-based credential rotation |
| `DBIO::AccessBroker::HostBound` | HostBound.pm | One credential pinned to one host (via `for_host`) |
### Storage Integration
Storage detects broker via `_is_access_broker_connect_info([$broker])` â true if single blessed element. Then:
1. `set_access_broker($broker)` â attaches broker to storage
2. `_current_dbi_connect_info` â `current_access_broker_connect_info`
3. `current_connect_info_for_storage($storage)` â `connect_info_for` (or storage-aware variant)
4. Broker returns HASHREF â Storage normalizes to internal format
`DBIO::Replicated` passes a broker (or a `for_host` view) through the master/replicant connect paths untouched â the per-backend `Storage::DBI` consumes it. The guard predicate is `DBIO::Util::is_access_broker($x)`.
## OOP
- Core: `Class::Accessor::Grouped` + `Class::C3::Componentised` â [[dbio-perl-class-patterns]]
- Pure-Perl style baseline â [[dbio-perl-syntax]]
- Drivers: Moo (PostgreSQL) or Moose (Replicated) â match existing driver
- `DBIO::Moo`/`DBIO::Moose` = optional bridges (`suggests`), FOREIGNBUILDARGS + lazy rules â [[dbio-moo-moose]]
## Driver contract versioning (ADR 0024)
The five base classes out-of-tree drivers subclass carry
`$CONTRACT_VERSION` and a `contract_version()` accessor:
- `DBIO::Introspect::Base`
- `DBIO::Diff::Base`
- `DBIO::Deploy::Base`
- `DBIO::SQLMaker`
- `DBIO::Storage::DBI::Capabilities`
The contract version advances only when the public shape changes in a
way drivers can observe (new method, new capability, signature change).
Current contract: 1.1 (post F02/F10/F12). Tripwire:
`t/test/12_contract_version.t`.
Drivers record what they were tested against and warn / strict-fail at
load time when it drifts:
```perl
our $TESTED_AGAINST_CONTRACT = '1.1';
if (DBIO::Storage::DBI::Capabilities->contract_version
ne $TESTED_AGAINST_CONTRACT) {
warnings::warn "DBIO contract drift: wrote against 1.1, core now "
. "ships " . DBIO::Storage::DBI::Capabilities->contract_version;
}
```
## DDL transactional safety + IF EXISTS (ADR 0026)
( run in 1.157 second using v1.01-cache-2.11-cpan-f52f0507bed )