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 inCHARACTER(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
serverrefusal came, and met the convention. - pending
-
A declared
pendingrefusal 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
deviationshowed, as declared. Carriesreason. - todo
-
Would be
fail, for a case newer than "conformance_answered_through" outsideAUTHOR_TESTINGandRELEASE_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_classis 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.$caseis the case hash, for a driver that needs its name. - conformance_cleanup($schema, $case, $namespace)
-
Remove what the case created, and disconnect.
$namespaceis 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".
%argscarries what the case named (source,column,columns,namespace, ...) andtable, 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.
$schemais the connected schema of thefromdeclaration, installed, with the case's rows in it.$to_classis a fresh DBIO::Schema subclass with the result classes of thetodeclaration 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 (upgradeof 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$schemaitself 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 itsschema). The default returnsundef: the deploy cases are thennafor the driver. The runner callsapplyandupgradeon 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 aDROP. - conformance_quote_ident($schema, $name)
-
Return
$namequoted 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 -- thequote_charof its SQL maker (one character, or an opening and a closing one); without aquote_charthe name stays as it is. - conformance_create($schema, $source, \%values)
-
createa row of$sourcethrough 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
$sourcematching%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
$sourcematching%condition, read fresh from the server, as a hashref of its columns;undefwhen none matches. - conformance_update($schema, $source, \%condition, \%set)
-
Update the matching rows with one
UPDATE.%setis 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
populatedoes in void context.@rowsis 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
{}.DiffandBindare known: a driver without a Diff declaressub 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'ssubject, the reason), and the result isrefusedorpending. 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_TESTINGandRELEASE_TESTING, the cases running asTODO. 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_namereturns 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). Statusrefused.
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.
cardis required: a card of the driver's own board, in the formk34. 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 (
subjectof the case), the column -- exactly: the column's name stands in the text as a whole identifier, soidis not named byuser_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:
isbyeq(undefmeans NULL),isntbyne,likeby regex. - rejects
-
{ rejects => { source => 'Pair', first => { ... }, create => { ... } } }first(optional) is created and must succeed; thencreatemust 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 bycolumn = valueinthrough(defaultsource). - 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
linkcolumns 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 byeq, array refs elementwise,undeffor "not there". A fact about a source the upgrade dropped (sourceis the moniker of thefromdeclaration, which the upgraded schema no longer knows) carries the table name the case declared astable. - 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
findis read fresh ("conformance_find"); it must exist and the named columns must equalis(undefmeans NULL). Withoutisonly its existence is checked. - count
-
{ count => { source => 'Base', where => { name => 'x' }, is => 1 } }The number of rows of
sourcematchingwhere(all when absent) isis. - bind_roundtrip
-
{ bind_roundtrip => { source => 'Val', column => 'v', values => [ '01234', '007' ], ops => [ qw(create find search update populate) ] } }For a Bind case, each of
valuesgoes throughcolumn(the other columns of the source must be nullable; the key is the first primary key column, which the runner fills) by each operation ofops, 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
-
createa row holding the value ("conformance_create"); the row read back holds it. - find
-
The row found by the value ("conformance_find") holds it.
- search
-
Exactly as many rows are found by the value ("conformance_count") as rows hold it at that point: the ones
create,updateandpopulatemade. - update
-
createa row with the column NULL,updateit to the value ("conformance_update"), read it back by its key. - populate
-
populatea 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 storesvalueand the hash the caller passed still holds it, not a placeholder; apopulateof three rows mixing NULL and the value stores each as given and leaves the caller's rows holding what they held.label_columnis 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 bindvaluefinds one row andothernone, 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', orundefwhen the server holds no such object. - table_name
-
source. The table name as Introspect reads it back, exactly (case, spaces, quote characters),undefwhen absent. - table_in_namespace
-
source,namespace,unqualified. 1 when the tableunqualifiedis in namespacenamespace, 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.