Skip to content

handle complex foreign key associations #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 83 additions & 11 deletions lib/DBIx/DataModel/Schema/Generator.pm
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ sub parse_SQL_Translator {
role => $role,
mult_min => 0,
mult_max => '*',
is_cascade => lc($fk->on_delete) eq 'cascade', # use fc on perl > 5.16.
}
);
push @{$self->{assoc}}, \@assoc;
Expand All @@ -294,14 +295,85 @@ sub perl_code {
or croak "can't generate schema: no data. "
. "Call parse_DBI() or parse_DBIx_Class() before";

# make sure there is no duplicate role on the same table
my %seen_role;

# [1] a table which has fk cascades to more than one table must be an
# Association

# [2] a table which has multiple fk cascades to a single table (even
# if it has fk non-cascades) is a Composition.

# [3] a table with multiple fk cascades to a single ref table combines
# those cascades into a single Composition

# [4] a table with fk cascades and non-cascades to a single ref table
# splits the cascades into a Composition and the non-cascades to an Association


my %tblassoc;
foreach my $assoc (@{$self->{assoc}}) {
my $count;
$count = ++$seen_role{$assoc->[0]{table}}{$assoc->[1]{role}};
$assoc->[1]{role} .= "_$count" if $count > 1;
$count = ++$seen_role{$assoc->[1]{table}}{$assoc->[0]{role}};
$assoc->[0]{role} .= "_$count" if $count > 1;
my ( $t0, $t1 ) = @{ $assoc };

# separate cascades from non cascades, group by tables
# $tblassoc{t1}[is_cascade ? 0 : 1 ]{t0}[ @assocs ];
my $tassoc =
(
(
$tblassoc{ $t1->{table} } ||= [ ]
)->[ !!$t1->{is_cascade} || 0 ] ||= {}
)->{ $t0->{table} } ||= [];

push @{ $tassoc }, $assoc;
}


my @relationship = ( 'Association', 'Composition' );

my @associations; # final list of associations;
my %seen_role; # ensure role names are unique;

for my $t1 ( values %tblassoc ) {

# [1]
my $cascades = $t1->[1];

if ( keys %$cascades > 1 ) {

# make them all Associations
while( my ( $t0, $assocs ) = each %{$cascades } ) {

push @{ $t1->[0]{$t0} }, @$assocs;

}

$t1->[1] = {};
}

# Merge multiple associations between two tables. Assumes that
# multiplicities are the same for the associations.

for my $ridx ( 0..1 ) {

my $rel = $t1->[$ridx];

while( my ( $t0, $assocs ) = each %{ $rel } ) {

# use the first association as a template
my $assoc = $assocs->[0];

for my $tidx ( 0..1 ) {

# combine columns
$assoc->[$tidx]->{col} = join( ' ', map { $_->[$tidx]{col} } @$assocs );

my $count = ++$seen_role{$assoc->[$tidx]->{table}}{$assoc->[1-$tidx]->{role}};
$assoc->[1-$tidx]->{role} .= "_$count" if $count > 1;
}

# add it to the final list of associations
push @associations, [ $relationship[$ridx], $assoc ];

}
}
}

# compute max length of various fields (for prettier source alignment)
Expand Down Expand Up @@ -360,7 +432,10 @@ __END_OF_CODE__
$code .= sprintf("# $colsizes\n", qw/Class Role Mult Join/)
. sprintf("# $colsizes", qw/===== ==== ==== ====/);

foreach my $a (@{$self->{assoc}}) {
foreach my $assoc (@associations) {

my ( $relationship, $a ) = @$assoc;


# for prettier output, make sure that multiplicity "1" is first
@$a = reverse @$a if $a->[1]{mult_max} eq "1"
Expand All @@ -373,9 +448,6 @@ __END_OF_CODE__
$a->[$i]{mult} = {"0..*" => "*", "1..1" => "1"}->{$mult} || $mult;
}

# association or composition
my $relationship = $a->[1]{is_cascade} ? 'Composition' : 'Association';

$code .= "\n->$relationship(\n"
. sprintf($format, @{$a->[0]}{qw/table role mult col/})
. ",\n"
Expand Down
124 changes: 116 additions & 8 deletions t/v2_generator.t
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use warnings;

use DBIx::DataModel::Schema::Generator;

use constant NTESTS => 7;
use constant NTESTS => 19;

use Test::More tests => NTESTS;


Expand Down Expand Up @@ -45,6 +46,26 @@ SKIP: {
emp_id INTEGER NOT NULL REFERENCES employee(emp_id),
status_name TEXT
);

-- two foreign keys to one reftable, one cascades
CREATE TABLE fk_reftable_1 (
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status),
emp_id INTEGER NOT NULL REFERENCES employee_status(emp_id) ON DELETE CASCADE
);

-- two foreign keys to one reftable, both cascade
CREATE TABLE fk_reftable_2 (
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status) ON DELETE CASCADE,
emp_id INTEGER NOT NULL REFERENCES employee_status(emp_id) ON DELETE CASCADE
);

-- two foreign keys to two reftables, both cascade
CREATE TABLE fk_reftable_3 (
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status) ON DELETE CASCADE,
emp_id INTEGER NOT NULL REFERENCES employee(emp_id) ON DELETE CASCADE
);


});

my $generator = DBIx::DataModel::Schema::Generator->new(
Expand All @@ -54,11 +75,98 @@ SKIP: {
$generator->parse_DBI($dbh);
my $perl_code = $generator->perl_code;

like($perl_code, qr{Table\(qw/Activity}, "Table Activity");
like($perl_code, qr{Table\(qw/ActivityEvent}, "Table ActivityEvent");
like($perl_code, qr{Composition.*?activity_events}s, "Composition");
like($perl_code, qr{Association.*?activit(ie|y)s}s, "Association");
like($perl_code, qr{employee_2}s, "avoid duplicate associations");
sub match_entry {

# $type, [ $class, @etc ], [ ... ], $msg
my $msg = pop;
my $type = quotemeta(shift);

# match start and end of an line, depends if there are one or two lines per entry
my ( $start, $end ) = ( @_ > 1) ? ( qr{\[qw/\s*}, qr{\s*/\]} ) : ( qr{qw/\s*}, qr{\s*/} ) ;

my $re = join('',
qr{$type\s*\(\s*},
join( qr{\s*,\s*}, # join multiple lines
map {
join( '',
$start,
join( qr/\s+/,
map { defined($_) ? quotemeta( $_ ) # so multiplicity of '*' passes through
: qr{[^)]*?} # undef means match to end of line (matches any char except right paren)
} @{$_},
),
$end,
)
} @_ # iterate over lines
),
);


like( $perl_code, qr/$re/, $msg );
}

# ensure Tables are created
match_entry( 'Table', [ $_, undef ], "created Table $_" )
foreach qw[ Activity ActivityEvent Department Employee EmployeeStatus FkReftable1 FkReftable2 FkReftable3 ];

match_entry( 'Association',
[ qw( Employee employee 1 emp_id emp_id ) ],
[ qw( Activity activities * supervisor emp_id ) ],
'Merged Association',
);


match_entry( 'Association',
[ qw( Department department 1 dpt_id ) ],
[ qw( Activity activities * dpt_id ) ],
'Association: Department, Activity',
);


match_entry( 'Composition',
[ qw( Activity activity 1 act_id ) ],
[ qw( ActivityEvent activity_events * act_id ) ],
'Composition Activity, ActivityEvent'
);

match_entry( 'Association',
[ qw( Employee employee 1 emp_id ) ],
[ qw( EmployeeStatus employee_statuses * emp_id ) ],
'Association: Employee, EmployeeStatus',
);

match_entry( 'Association',
[ qw( EmployeeStatus employee_status 1 emp_id_status ) ],
[ qw( FkReftable1 fk_reftable_1s * emp_id_status ) ],
'Association: two foreign keys to one reftable, one cascades',
);

# checks for duplicate role as well.
match_entry( 'Composition',
[ qw( EmployeeStatus employee_status_2 1 emp_id ) ],
[ qw( FkReftable1 fk_reftable_1s_2 * emp_id ) ],
'Composition: two foreign keys to one reftable, one cascades',
);

match_entry( 'Composition',
[ qw( EmployeeStatus employee_status 1 emp_id emp_id_status ) ],
[ qw( FkReftable2 fk_reftable_2s * emp_id emp_id_status ) ],
'Merged Composition: two foreign keys to one reftable, both cascade',
);

match_entry( 'Association',
[ qw( EmployeeStatus employee_status 1 emp_id_status ) ],
[ qw( FkReftable3 fk_reftable_3s * emp_id_status ) ],
'Forced Association: two foreign keys to two reftables, both cascade (1)',
);


match_entry( 'Association',
[ qw( Employee employee 1 emp_id ) ],
[ qw( FkReftable3 fk_reftable_3s * emp_id ) ],
'Forced Association: two foreign keys to two reftables, both cascade (2)',
);


# diag($perl_code);

Expand All @@ -78,8 +186,8 @@ SKIP: {
);
$generator->parse_DBI($dbh);
$perl_code = $generator->perl_code;
like($perl_code, qr{Table\(qw/Foo}, "Table foo");
like($perl_code, qr{Table\(qw/Bar}, "Table bar");
like($perl_code, qr{Table\(qw/Foo}, "created Table foo");
like($perl_code, qr{Table\(qw/Bar}, "created Table bar");
}