NAME

DBIO::Test::Conformance - Shared case table runner: a driver answers every case: correctly, by refusing, or by a declared deviation

VERSION

version 0.900004

SYNOPSIS

package DBIO::Foo::Test::Conformance;
use base 'DBIO::Test::Conformance';

sub conformance_live_available { $ENV{DBIO_TEST_FOO_DSN} ? 1 : 0 }

sub conformance_connect {
  my ($class, $schema_class) = @_;
  $schema_class->load_components('Foo');
  return $schema_class->connect(@ENV{qw(DBIO_TEST_FOO_DSN DBIO_TEST_FOO_USER DBIO_TEST_FOO_PASS)});
}

sub conformance_install { my ($class, $schema, $case) = @_; $schema->deploy }
sub conformance_cleanup { my ($class, $schema, $case, $ns) = @_; ... drop what the case created, found by $ns; nothing without a mark ... }
sub conformance_fact    { my ($class, $schema, $name, $args) = @_; ... read it back through Introspect ... }
sub conformance_diff_is_empty { my ($class, $schema, $case) = @_; ... }

sub conformance_answered_through { { DDL => 1, Diff => 1, Bind => 1 } }

sub conformance_refusals {{
  'ddl-foreign-key' => {
    kind   => 'pending',                       # not built yet
    card   => 'k34',                           # required for pending
    reason => qr/foreign keys are not emitted yet/,
  },
  'ddl-ident-special-chars' => {
    kind   => 'server',                        # this engine cannot do it
    reason => qr/cannot hold a quote character in a name/,
  },
  'ddl-type-size-char' => {
    kind   => 'deviation',                  # the engine does it differently
    fact   => qr/accepted a row it must reject/,   # what the run observes
    reason => 'the engine keeps the declared length but does not enforce it',
  },
}}

DESCRIPTION

One list of cases, stated once in core, answered by every driver (ADR 0060). A case is a schema declaration in DBIO's terms plus what must be observable on the server afterwards. This module is the runner: it builds the result classes of a case, hands them to the driver through a handful of seams, checks the expectation and reports one answer per case.

There are exactly three acceptable answers:

correct

The default. The case runs and its expectation holds. Needs no declaration.

refuses

The driver decides, before it emits anything it knows the server will reject, to throw -- and says so in the driver's declaration ("conformance_refusals"). A declared refusal is a test, not a skip: the case is driven, the exception has to come and has to meet "The refusal convention", and a declared refusal that succeeds is a failure.

deviates

The engine does something else than the case expects, and the driver says exactly what, and why that is accepted, in the same declaration (kind => 'deviation', "Declaring refusals"). The case is driven, its expectation has to fail in the declared way, and a deviation the case no longer shows is a failure. Where a refusal is the driver's decision to throw, a deviation is the server's behaviour taken as it is: SQLite stores 11 characters in CHARACTER(10) and has no check to say otherwise. A deviation answers the case like the other two ("conformance_answered_through").

Silently wrong results nobody declared, SQL the server rejects and a server error handed through are not answers; they are failures. There is no per-case skip.

A driver subclasses this, answers the seams of "WRITING A DRIVER'S CLASS" and ships three lines of test file:

use DBIO::SQLite::Test::Conformance;
DBIO::SQLite::Test::Conformance->run('DDL');

There are three lists, one per area, and a driver runs each in a test file of its own:

DDL

DBIO::Test::Conformance::Cases::DDL: one declaration is installed; the server shows what it became.

Diff

DBIO::Test::Conformance::Cases::Diff: a declaration is installed, rows are loaded, the database is upgraded to a second declaration; the server shows the result and the rows.

Bind

DBIO::Test::Conformance::Cases::Bind: values go through a column by every operation of the ResultSet API and must come back as written.

Names

DBIO::Test::Conformance::Cases::Names: a schema name, an extension name and a load library, each as a plain name and as a name with a quote, a semicolon, a blank, a backslash or a line break ("Names").

Each list has a table version of its own ("table_version") and a driver answers each through a version of its own ("conformance_answered_through"). Core's own test of this class uses a mock driver and never a database; a conformance run against a server belongs in the driver's dist.

METHODS

table_version

my $v = DBIO::Test::Conformance->table_version;          # DDL
my $v = DBIO::Test::Conformance->table_version('Diff');

The version of the case table of an area, DDL without an argument. Each list has its own version and starts at 1. A case carries the version that added it as since; a driver declares the version it answered through, per area, as "conformance_answered_through". Throws for an unknown area.

cases

my $cases = DBIO::Test::Conformance->cases('DDL');

The arrayref of cases of an area, a fresh copy. Throws for an area without a list.

conformance_live_available

Seam, must be provided. See "WRITING A DRIVER'S CLASS".

conformance_connect

Seam, must be provided.

conformance_install

Seam, must be provided.

conformance_cleanup

Seam, must be provided. Removes only what the case created, found by the mark "conformance_namespace" returned; with no mark it removes nothing. The runner does not call it when conformance_namespace threw.

conformance_fact

Seam, must be provided.

conformance_diff_is_empty

Seam, must be provided.

conformance_upgrade

Seam, must be provided by a driver that runs the Diff list. See "WRITING A DRIVER'S CLASS".

conformance_deploy

Seam with a default: undef, so the deploy cases of the Diff list are not applicable. See "WRITING A DRIVER'S CLASS".

conformance_apply_name

Needed to run the Names list for a kind the driver declares ("conformance_name_kinds"). See "Names".

conformance_name_kinds

Seam with a default: no kind, so every Names case is not applicable. A hash ref of the kinds the engine has: schema (a namespace the connect key schema names), extension (a database extension by name), library (a library an extension loads).

conformance_namespace

Seam with a default: none. Returns the mark conformance_cleanup works from; throws when no empty namespace can be had.

conformance_quote_ident

Seam with a default: the quoting rule of the connected storage's SQL maker (quote_char, a single character or an opening and a closing one); without one, $name unchanged. A closing quote character inside $name is doubled (] for the pair [, ]; the character itself for a single one).

conformance_refusals

Seam with a default: no refusals.

conformance_refused_areas

Seam with a default: no area refused.

conformance_answered_through

Seam with a default: 0. A scalar counts for the DDL list only; a hash ref gives the version per area.

conformance_create

Seam with a working default through the ResultSet API.

conformance_count

Seam with a working default through the ResultSet API.

conformance_delete

Seam with a working default through the ResultSet API.

conformance_find

my $row = $class->conformance_find($schema, $source, \%condition);

Seam with a working default through the ResultSet API: the first row of $source matching %condition, read fresh from the server, as a hashref of its columns; undef when none matches.

conformance_update

my $n = $class->conformance_update($schema, $source, \%condition, \%set);

Seam with a working default through the ResultSet API: update the matching rows with one UPDATE. %set is passed as the caller's hash, not a copy.

conformance_populate

$class->conformance_populate($schema, $source, \@rows);

Seam with a working default through the ResultSet API: insert the rows (hash refs, all with the same keys) in void context. @rows is the caller's array; the runner checks that it still holds the values afterwards.

check_declarations

my @problems = $class->check_declarations('DDL');

What can be checked without a server: every declared refusal names a known case, has kind server, pending or deviation; a regex reason and a card for pending; a regex fact and a non-empty string reason for deviation (not allowed for an area); the same for every refused area; conformance_answered_through is a non-negative integer. Returns the problems as strings, none when the declaration is whole. "run" makes it the first test.

results

my @results = $class->results('DDL');
my @results = $class->results('DDL', only => [ 'ddl-foreign-key' ]);

Drive every case of the area and return one hashref per case: { case, status, message } and, for pending, card, for deviation, reason. It does not touch Test::More. status is

pass

The case is correct.

refused

A declared server refusal came, and met the convention.

pending

A declared pending refusal came, and met the convention.

fail

Anything else: a wrong result, an unexpected or malformed exception, a stale refusal, a failed cleanup, a diff that is not empty.

deviation

A declared deviation showed, as declared. Carries reason.

todo

Would be fail, for a case newer than "conformance_answered_through" outside AUTHOR_TESTING and RELEASE_TESTING.

na

A Names case of a kind the driver does not declare ("Names"), or a deploy case of the Diff list for a driver without "conformance_deploy".

only restricts the run to the named cases.

summary_line

my $line = $class->summary_line(\@results);

The one line that closes a run: a refused area once (area, kind, the card of a pending one or the reason of a server one, and the number of its cases), the other pending refusals with their cards, the deviations with their reasons, and the cases still newer than the answered table version. A case is named only where it was refused by itself ("conformance_refusals"), never once per case of a refused area. undef when there is nothing to say.

run

DBIO::Foo::Test::Conformance->run('DDL');

Write the tests: first one for the declarations ("check_declarations"), then, when "conformance_live_available", one per case; a todo case runs as a TODO test. The closing summary line is printed with diag. Without a server only the declarations are checked and the number of cases not run is said. Does not call done_testing.

expectation_kinds

my @kinds = DBIO::Test::Conformance->expectation_kinds;

The expectation kinds a case may use, sorted.

WRITING A DRIVER'S CLASS

Name it DBIO::<Driver>::Test::Conformance and keep it under lib/, so an extension dist can reuse it. Every method is a class method.

Seams that must be provided (the base dies naming the seam and the class):

conformance_live_available

True when there is a server to run against: the driver's DBIO_TEST_* variables, and its DBD present. A driver with no server to gate on (an in-memory SQLite) answers true.

conformance_connect($schema_class)

$schema_class is a fresh DBIO::Schema subclass with the case's result classes registered. Load the driver component into it, connect, and return the connected schema. Never connect the class twice; every case gets its own.

conformance_install($schema, $case)

Install the case's declaration through the driver's public entry point ($schema->deploy, or what the driver's deploy class exposes). Throw the driver's refusal from here when the driver cannot do the case. $case is the case hash, for a driver that needs its name.

conformance_cleanup($schema, $case, $namespace)

Remove what the case created, and disconnect. $namespace is the mark "conformance_namespace" returned; remove only what that mark identifies, and nothing when it identifies nothing. Runs after every case that got its namespace, also after a failure of the install or a check; it is not called when "conformance_namespace" threw (the runner closes the connection then).

conformance_fact($schema, $name, \%args)

Measure one fact the server shows, through the driver's Introspect, and return it. The vocabulary is under "Facts". %args carries what the case named (source, column, columns, namespace, ...) and table, the table name the source was declared with. An unknown fact name is the driver's programming error and throws.

conformance_diff_is_empty($schema, $case)

True when a diff of the installed database against the same declaration is empty. It is asked after every case that otherwise passed (the postcondition "install, then diff against the same declaration, is empty") unless the driver declares the Diff area refused ("conformance_refused_areas"). It is asked after the upgrade of a Diff case as well (postcondition 2), of the schema "conformance_upgrade" returned, and is not asked of a Bind case.

conformance_upgrade($schema, $to_class, $case)

Needed to run the Diff list. $schema is the connected schema of the from declaration, installed, with the case's rows in it. $to_class is a fresh DBIO::Schema subclass with the result classes of the to declaration registered and not connected (the view definitions in it are resolved). Connect it to the same database -- load the driver component into it as "conformance_connect" does, and where the engine's database is the connection (an in-memory SQLite) share the handle -- run the upgrade through the driver's public entry point (upgrade of the deploy class) and return the connected schema of $to_class. Throw the driver's refusal from here when the driver cannot do the case. The runner checks the expectations and the diff postcondition against the returned schema, hands it to "conformance_cleanup", and closes $schema itself afterwards; so cleanup must not close a handle the other schema still needs, but may close its own.

conformance_deploy($schema, $to_class, $case)

Optional, for the deploy cases of the Diff list (diff-deploy-*, table version 4). Same arguments and same connecting as "conformance_upgrade", but instead of upgrading, return the driver's deploy object over the connected target schema: DBIO::Foo::Deploy->new(schema => $to) (a DBIO::Deploy::Base; the runner reads the schema from its schema). The default returns undef: the deploy cases are then na for the driver. The runner calls apply and upgrade on the object and checks the database through the facts; it needs nothing else from the driver.

Seams with a working default:

conformance_namespace($schema, $case)

Give the case an empty namespace -- a schema on PostgreSQL, a database on MySQL, a table-name prefix where the engine has nothing else -- and return a mark for "conformance_cleanup", which removes only what that mark identifies. Throw when no empty namespace can be had (the database or schema is not empty): the case is then reported as failed and "conformance_cleanup" is not called. Default: none, returns undef. Necessary whenever a diff compares the whole visible database, so that nothing another case left behind comes back as a DROP.

conformance_quote_ident($schema, $name)

Return $name quoted the way this driver writes an identifier into SQL. The runner builds the definition of a view from identifiers it quotes through this hook, because a view definition is SQL of the user, which the driver does not rewrite. Default: the quoting rule of the connected storage -- the quote_char of its SQL maker (one character, or an opening and a closing one); without a quote_char the name stays as it is.

conformance_create($schema, $source, \%values)

create a row of $source through the ResultSet API, read it back fresh from the server, and return the columns as a hashref.

conformance_count($schema, $source, \%condition)

The number of rows of $source matching %condition.

conformance_delete($schema, $source, \%condition)

Delete the matching rows with one DELETE, leaving cascades to the server.

conformance_find($schema, $source, \%condition)

The first row of $source matching %condition, read fresh from the server, as a hashref of its columns; undef when none matches.

conformance_update($schema, $source, \%condition, \%set)

Update the matching rows with one UPDATE. %set is the caller's hash, not a copy: the runner checks that it still holds the values afterwards.

conformance_populate($schema, $source, \@rows)

Insert the rows (hash refs with the same keys), the way populate does in void context. @rows is the caller's array; the runner checks that it still holds the values afterwards.

conformance_refusals

The hashref of declared refusals, default {}. See "Declaring refusals".

conformance_refused_areas

The hashref of areas the driver does not do at all, default {}. Diff and Bind are known: a driver without a Diff declares

sub conformance_refused_areas {{
  Diff => { kind => 'pending', card => 'k36', reason => qr/diff is not built yet/ },
}}

and the diff postcondition of the DDL list is then not asked of it. The declaration is checked like any other ("check_declarations").

The runner treats every case of the refused area as a declared refusal of that kind, with that card and reason, unless the case has a refusal of its own: the case is driven, the driver's seam has to throw a refusal that meets "The refusal convention" (stem refus, the table of the case's subject, the reason), and the result is refused or pending. A case of the area that runs through is a stale refusal and fails. The area is a refusal, never a deviation.

conformance_answered_through

The table version ("table_version") the driver's answers were written against. Default 0: a driver that does not declare it has answered nothing, and every case counts as newer.

A scalar is the version of the DDL list and says nothing about the others, which then count as 0. A hash ref gives it per area:

sub conformance_answered_through { { DDL => 1, Diff => 1 } }

An area the hash does not name is 0. A driver that answers a list of an area for which it is at 0 stays green outside AUTHOR_TESTING and RELEASE_TESTING, the cases running as TODO. See "New cases and released drivers".

Names

A schema name (the connect key schema), the name of a database extension and the name of a library an extension loads are identifiers (ADR 0077). The driver writes one into a statement only through its identifier quoting or a bind, or refuses it. The Names list drives each kind with a plain name and with five names a statement built by concatenation cannot take. There are two answers, neither declared:

set

The server shows the name as given: conformance_apply_name returns it as introspection reads it back.

refused

The driver throws a DBIO::Exception (the check core makes of ;, a line break, CR and NUL counts: the driver need not repeat it). Status refused.

An error of the server (DBIO::Exception::Server: a syntax error), another name than the one given and an exception that is not a DBIO exception are failures.

The driver declares the kinds its engine has with conformance_name_kinds (schema, extension, library); a case of a kind it does not declare has status na and is green. This is the capability, not a skip of a case. conformance_apply_name($kind, $name) does the whole of one case: connects a fresh connection with the connect key (schema), declares the extension or activates the library, reads the name back through introspection, removes what it created, disconnects, and returns the name read back. A refusal declaration ("Declaring refusals") does not exist for this list; a refusal is an answer.

Declaring refusals

conformance_refusals returns a hashref keyed by case name:

{ case_name => { kind => 'server' | 'pending', reason => qr/.../, card => 'k34' } }
{ case_name => { kind => 'deviation', fact => qr/.../, reason => 'why' } }
kind server

The engine cannot do it. Permanent; needs no card.

kind pending

The driver does not do it yet. card is required: a card of the driver's own board, in the form k34. That line separates "not built" from "does not exist on this server" and makes the backlog readable from the declaration.

reason

For a refusal, a regex the refusal's message must match.

kind deviation

The engine answers the case's expectation differently, permanently, and the driver accepts that. The case runs to the end without throwing and its expectation fails; the declaration says how:

fact

A regex the failure text of the run must match. It is the statement of what the server does, in the words of the runner's own failure message, so a different wrong result is not hidden behind the declaration.

reason

A non-empty string: why the deviation is accepted. A deviation without one is rejected by "check_declarations".

An exception is not a deviation, and a case that runs correctly under a declared deviation fails as a stale deviation. A refused area is never a deviation.

pending is green. The run ends with one line, per refusal and its card, printed by "run" and returned by "summary_line":

3 pending refusals: ddl-foreign-key (karr k34), ddl-fk-actions (karr k35), ...

A refused area ("conformance_refused_areas") is one entry, not one per case:

Diff area pending (karr k36): 20 cases

and, for a server-kind area, Diff area refused by the server (reason): 20 cases. A case is named in the line only where it carries a refusal of its own.

and, for deviations, with their reasons:

1 deviation: ddl-type-size-char (the engine keeps the declared length but does not enforce it)

The refusal convention

A refusal is a DBIO exception, thrown through $schema->throw_exception (or DBIO::Exception->throw) by the driver itself -- before the server is asked. Its text

  • contains the stem refus (case-insensitive: refuses, refused, refusal),

  • names the table, as the case declared it, and, when the case has a column (subject of the case), the column -- exactly: the column's name stands in the text as a whole identifier, so id is not named by user_id,

  • matches the declared reason.

It is not a DBIO::Exception::Server and not a bare string: a server error that reaches the caller is the server's decision, a refusal is the driver's. The runner checks exactly these and names the part that is missing.

New cases and released drivers

Every case carries since, the table version of its list that added it. A case with since greater than "conformance_answered_through" for its area is still driven and reported, but a failure of it only turns the suite red under AUTHOR_TESTING or RELEASE_TESTING; otherwise it runs as a TODO and the summary line counts it. A driver raises the number when its answers are complete, deliberately, in the same change.

CASES

A case is a hash (see DBIO::Test::Conformance::Cases::DDL):

{ name, since, cards, subject, input => { sources => [ ... ] }, expect => [ ... ] }

The input of a Diff case has two declarations and rows (DBIO::Test::Conformance::Cases::Diff):

input => {
  from => { sources => [ ... ] },
  to   => { sources => [ ... ] },
  rows => [ { source => 'Parent', values => { id => 1 } }, ... ],
}

from and to are sources as below. The runner installs from, creates rows in order through "conformance_create", has the driver upgrade to to ("conformance_upgrade") and checks expect against the database as it is then. The input of a Bind case is a DDL input (DBIO::Test::Conformance::Cases::Bind).

Input

input->{sources} is a list of sources in declaration order. A source:

{
  name          => 'Child',                       # the moniker
  table         => 'child',
  columns       => [ id => { data_type => 'integer' }, ... ],   # ordered pairs
  primary_key   => [ 'id' ],                      # optional
  unique        => [ [ constraint_name => [ 'a', 'b' ] ], ... ],
  relationships => [ { type => 'belongs_to', name => 'parent',
                       source => 'Parent', cond => 'parent_id',
                       attrs => { on_delete => 'CASCADE' } }, ... ],
  indexes       => [ { name => 'idx', columns => [ ... ], unique => 0 }, ... ],
  view          => { definition => 'SELECT ...', virtual => 0 },   # makes it a view
}

In the definition of a view, every {{name}} is an identifier: the runner replaces it, after the connect, by name quoted through "conformance_quote_ident" (SELECT {{id}} FROM {{base}}).

Column definitions use the base types of DBIO::Schema::Type, and varchar with a size for a bounded string, which every driver writes. source in a relationship is the moniker of another source of the case. The runner builds these as result classes in a package of its own; the case never contains SQL.

Expectations

Every entry of expect is a hash with one key, the kind. The expectation is what the server shows, never a piece of SQL text.

read_back
{ read_back => { source => 'Note', create => { id => 1 },
                 is => { note => "it's" }, isnt => { ... }, like => { ... } } }

Create the row, read it fresh from the server, compare the named columns: is by eq (undef means NULL), isnt by ne, like by regex.

rejects
{ rejects => { source => 'Pair', first => { ... }, create => { ... } } }

first (optional) is created and must succeed; then create must be rejected by the server. Accepting it is a failure.

search_by
{ search_by => { source => 'Order', create => { ... },
                 through => 'View', column => 'group', value => 'h' } }

Create the row in source, then exactly one row must be found by column = value in through (default source).

auto_key
{ auto_key => { source => 'Counter', create => { name => 'x' } } }

Create without the primary key; the row read back has every key column set.

cascade_delete
{ cascade_delete => { parent => 'Parent', parent_values => { id => 1 },
                      child => 'Child', child_values => { id => 1 },
                      link => { parent_id => 'id' } } }

Create the parent and a child whose link columns point at it (child column => parent column), delete the parent, and the child must be gone.

fact
{ fact => { name => 'column_size', source => 'Sized', column => 'c', is => 10 } }

A fact read through the driver's Introspect ("conformance_fact") must equal is: strings by eq, array refs elementwise, undef for "not there". A fact about a source the upgrade dropped (source is the moniker of the from declaration, which the upgraded schema no longer knows) carries the table name the case declared as table.

rows_kept
{ rows_kept => { source => 'Base', find => { id => 1 }, is => { name => 'x' } } }

For a Diff case: a row loaded before the upgrade is still there. The row matching find is read fresh ("conformance_find"); it must exist and the named columns must equal is (undef means NULL). Without is only its existence is checked.

count
{ count => { source => 'Base', where => { name => 'x' }, is => 1 } }

The number of rows of source matching where (all when absent) is is.

bind_roundtrip
{ bind_roundtrip => { source => 'Val', column => 'v',
                      values => [ '01234', '007' ],
                      ops => [ qw(create find search update populate) ] } }

For a Bind case, each of values goes through column (the other columns of the source must be nullable; the key is the first primary key column, which the runner fills) by each operation of ops, in order, and must come back exactly as written (eq, bytes for bytes; a failure says where the first difference is). Every operation goes through a data seam:

create

create a row holding the value ("conformance_create"); the row read back holds it.

find

The row found by the value ("conformance_find") holds it.

Exactly as many rows are found by the value ("conformance_count") as rows hold it at that point: the ones create, update and populate made.

update

create a row with the column NULL, update it to the value ("conformance_update"), read it back by its key.

populate

populate a row holding the value ("conformance_populate"), read it back by its key.

int_first

A statement that binds an integer (a count by key 0) runs before the next operation, for the faults where an earlier bind decides the next one's type.

bind_lob_ops
{ bind_lob_ops => { source => 'Doc', column => 'body',
                    label_column => 'label', value => $bytes } }

For a Bind case with a LOB column, the key an auto-increment: create({}) leaves the LOB NULL; an update of the LOB alone stores value and the hash the caller passed still holds it, not a placeholder; a populate of three rows mixing NULL and the value stores each as given and leaves the caller's rows holding what they held. label_column is a text column the rows are found by.

bind_literal
{ bind_literal => { source => 'Note', column => 'txt', create => { id => 1, txt => 'x' },
                    value => 'x', other => 'y',
                    literal => q{{{txt}} = ? AND 'a?b' = 'a?b' /* a ? */} } }

For a Bind case: create the row, then count with literal SQL \[ $literal, $bind ] as the condition. The bind value finds one row and other none, whatever ? the literal holds in a string or a comment. {{name}} in the literal is an identifier, quoted through "conformance_quote_ident".

Facts

object_kind

source. 'table', 'view', or undef when the server holds no such object.

table_name

source. The table name as Introspect reads it back, exactly (case, spaces, quote characters), undef when absent.

table_in_namespace

source, namespace, unqualified. 1 when the table unqualified is in namespace namespace, else 0.

column_names

source. The column names the server holds, as an array ref; the runner sorts both sides.

column_size

source, column. The declared size of the column.

column_auto_increment

source, column. 1 or 0.

primary_key_columns

source. The primary key's columns as an array ref, [] for none.

unique_exists

source, columns. 1 when a unique constraint over exactly these columns exists, else 0.

foreign_key_count

source. The number of foreign key constraints of the table.

index_count

source, columns. The number of indexes (the primary key excluded) over exactly these columns; the one backing a unique constraint or a foreign key counts, so a duplicate shows as 2.

The diff postcondition is not an expectation and is not written into a case: it holds for every DDL case (install, then diff against the same declaration, is empty) and every Diff case (upgrade, then diff against the target declaration, is empty), wherever the driver has a Diff.

Running a list over an async transport

An async transport (DBIO::Async, the ev dists) runs the Bind list over its own bind path (ADR 0060, decision 7). It does so with the same lists and the same runner: every operation a Bind case performs on data goes through one of the data seams, and nothing else of the database is touched.

seam                      the operation
conformance_create        create a row, read it back
conformance_find          the first row matching a condition, or undef
conformance_count         the number of rows matching a condition
conformance_update        update the matching rows
conformance_populate      insert a list of rows
conformance_delete        delete the matching rows

Each has a default over the synchronous ResultSet API. A transport's subclass overrides "conformance_connect" to connect through the transport and these seams to issue the operation through it and wait for the Future in the seam, returning what the synchronous default returns:

package DBIO::PostgreSQL::EV::Test::Conformance;
use base 'DBIO::PostgreSQL::Test::Conformance';

sub conformance_create {
  my ($class, $schema, $source, $values) = @_;
  return $class->_wait($schema->resultset($source)->create_async({ %$values }));
}
# ... conformance_find, _count, _update, _populate likewise

The install, namespace, fact and diff seams stay the driver's own and run on the blocking connection of the same driver; the Bind list asks no fact and no diff. A value must reach the server by the transport's bind path or the case fails; there is nothing in the runner to bypass it.

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.