Skip to content

Commit 33d50f9

Browse files
committed
Properly handle complex foreign key associations.
The previous commit didn't properly handle the case where a table had multiple foreign key associations (with and without cascades) to one or more tables. The new logic implements the following rules: * a table which has fk cascades to more than one table must be an Association * a table which has multiple fk cascades to a single table (even if it has fk non-cascades) is a Composition. * a table with multiple fk cascades to a single ref table combines those cascades into a single Composition * 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 Associations and Compositions are merged if possible.
1 parent a2b09b0 commit 33d50f9

File tree

2 files changed

+196
-27
lines changed

2 files changed

+196
-27
lines changed

lib/DBIx/DataModel/Schema/Generator.pm

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,27 +294,87 @@ sub perl_code {
294294
or croak "can't generate schema: no data. "
295295
. "Call parse_DBI() or parse_DBIx_Class() before";
296296

297-
# make sure there is no duplicate role on the same table
298297

299-
# if a table has multiple cascaded foreign keys make it part of an
300-
# Association, not a Composition, as a table can't be part of
301-
# multiple Compositions.
298+
my @relationship = qw( Association Composition );
302299

303-
my %seen_role;
304-
my %relationship;
305-
foreach my $assoc (@{$self->{assoc}}) {
306-
my $count;
307-
$count = ++$seen_role{$assoc->[0]{table}}{$assoc->[1]{role}};
308-
$assoc->[1]{role} .= "_$count" if $count > 1;
309-
$count = ++$seen_role{$assoc->[1]{table}}{$assoc->[0]{role}};
310-
$assoc->[0]{role} .= "_$count" if $count > 1;
300+
# make sure there is no duplicate role on the same table
301+
# handle complex fk cascades
311302

312-
$relationship{ $assoc->[1]{table} }{ $assoc->[1]{is_cascade} ? 'Composition' : 'Association' }++;
303+
my %tblassoc;
304+
foreach my $assoc (@{$self->{assoc}}) {
305+
my ( $t0, $t1 ) = @{ $assoc };
306+
307+
# separate cascades from non cascades, group by tables
308+
# $tblassoc{t1}[is_cascade ? 0 : 1 ]{t0}[ @assocs ];
309+
my $tassoc =
310+
(
311+
(
312+
$tblassoc{ $t1->{table} } ||= [ ]
313+
)->[ !!$t1->{is_cascade} || 0 ] ||= {}
314+
)->{ $t0->{table} } ||= [];
315+
316+
push @{ $tassoc }, $assoc;
313317
}
314318

315-
# an association which has more than one Composition reverts to an Association.
316-
$_ = $_->{Association} || $_->{Composition} != 1 ? 'Association' : 'Composition'
317-
for values %relationship;
319+
# [1] a table which has fk cascades to more than one table must be an
320+
# Association
321+
322+
# [2] a table which has multiple fk cascades to a single table (even
323+
# if it has fk non-cascades) is a Composition.
324+
325+
# [3] a table with multiple fk cascades to a single ref table combines
326+
# those cascades into a single Composition
327+
328+
329+
my @relationships = ( 'Association', 'Composition' );
330+
331+
my @associations; # final list of associations;
332+
my %seen_role; # ensure role names are unique;
333+
334+
for my $t1 ( values %tblassoc ) {
335+
336+
# [1]
337+
my $cascades = $t1->[1];
338+
339+
if ( keys %$cascades > 1 ) {
340+
341+
# make them all Associations
342+
while( my ( $t0, $assocs ) = each %{$cascades } ) {
343+
344+
push @{ $t1->[0]{$t0} }, @$assocs;
345+
346+
}
347+
348+
$t1->[1] = {};
349+
}
350+
351+
# Merge multiple associations between two tables. Assumes that
352+
# multiplicities are the same for the associations.
353+
354+
for my $ridx ( 0..1 ) {
355+
356+
my $rel = $t1->[$ridx];
357+
358+
while( my ( $t0, $assocs ) = each %{ $rel } ) {
359+
360+
# use the first association as a template
361+
my $assoc = $assocs->[0];
362+
363+
for my $tidx ( 0..1 ) {
364+
365+
# combine columns
366+
$assoc->[$tidx]->{col} = join( ' ', map { $_->[$tidx]{col} } @$assocs );
367+
368+
my $count = ++$seen_role{$assoc->[$tidx]->{table}}{$assoc->[1-$tidx]->{role}};
369+
$assoc->[1-$tidx]->{role} .= "_$count" if $count > 1;
370+
}
371+
372+
# add it to the final list of associations
373+
push @associations, [ $relationships[$ridx], $assoc ];
374+
375+
}
376+
}
377+
}
318378

319379
# compute max length of various fields (for prettier source alignment)
320380
my %l;
@@ -372,7 +432,10 @@ __END_OF_CODE__
372432
$code .= sprintf("# $colsizes\n", qw/Class Role Mult Join/)
373433
. sprintf("# $colsizes", qw/===== ==== ==== ====/);
374434

375-
foreach my $a (@{$self->{assoc}}) {
435+
foreach my $assoc (@associations) {
436+
437+
my ( $relationship, $a ) = @$assoc;
438+
376439

377440
# for prettier output, make sure that multiplicity "1" is first
378441
@$a = reverse @$a if $a->[1]{mult_max} eq "1"
@@ -385,8 +448,6 @@ __END_OF_CODE__
385448
$a->[$i]{mult} = {"0..*" => "*", "1..1" => "1"}->{$mult} || $mult;
386449
}
387450

388-
my $relationship = $relationship{$a->[1]{table}};
389-
390451
$code .= "\n->$relationship(\n"
391452
. sprintf($format, @{$a->[0]}{qw/table role mult col/})
392453
. ",\n"

t/v2_generator.t

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use warnings;
33

44
use DBIx::DataModel::Schema::Generator;
55

6-
use constant NTESTS => 7;
6+
use constant NTESTS => 17;
7+
78
use Test::More tests => NTESTS;
89

910

@@ -45,6 +46,26 @@ SKIP: {
4546
emp_id INTEGER NOT NULL REFERENCES employee(emp_id),
4647
status_name TEXT
4748
);
49+
50+
-- two foreign keys to one reftable, one cascades
51+
CREATE TABLE fk_reftable_1 (
52+
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status),
53+
emp_id INTEGER NOT NULL REFERENCES employee_status(emp_id) ON DELETE CASCADE
54+
);
55+
56+
-- two foreign keys to one reftable, both cascade
57+
CREATE TABLE fk_reftable_2 (
58+
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status) ON DELETE CASCADE,
59+
emp_id INTEGER NOT NULL REFERENCES employee_status(emp_id) ON DELETE CASCADE
60+
);
61+
62+
-- two foreign keys to two reftables, both cascade
63+
CREATE TABLE fk_reftable_3 (
64+
emp_id_status INTEGER NOT NULL REFERENCES employee_status(emp_id_status) ON DELETE CASCADE,
65+
emp_id INTEGER NOT NULL REFERENCES employee(emp_id) ON DELETE CASCADE
66+
);
67+
68+
4869
});
4970

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

57-
like($perl_code, qr{Table\(qw/Activity}, "Table Activity");
58-
like($perl_code, qr{Table\(qw/ActivityEvent}, "Table ActivityEvent");
59-
like($perl_code, qr{Composition.*?activity_events}s, "Composition");
60-
like($perl_code, qr{Association.*?activit(ie|y)s}s, "Association");
61-
like($perl_code, qr{employee_2}s, "avoid duplicate associations");
78+
sub match_entry {
79+
80+
# $type, [ $class, @etc ], [ ... ], $msg
81+
my $msg = pop;
82+
my $type = quotemeta(shift);
83+
84+
# match start and end of an line, depends if there are one or two lines per entry
85+
my ( $start, $end ) = ( @_ > 1) ? ( qr{\[qw/\s*}, qr{\s*/\]} ) : ( qr{qw/\s*}, qr{\s*/} ) ;
86+
87+
my $re = join('',
88+
qr{$type\s*\(\s*},
89+
join( qr{\s*,\s*}, # join multiple lines
90+
map {
91+
join( '',
92+
$start,
93+
join( qr/\s+/,
94+
map { defined($_) ? quotemeta( $_ ) # so multiplicity of '*' passes through
95+
: qr{[^)]*?} # undef means match to end of line (matches any char except right paren)
96+
} @{$_},
97+
),
98+
$end,
99+
)
100+
} @_ # iterate over lines
101+
),
102+
);
103+
104+
105+
like( $perl_code, qr/$re/, $msg );
106+
}
107+
108+
# ensure Tables are created
109+
match_entry( 'Table', [ $_, undef ], "created Table $_" )
110+
foreach qw[ Activity ActivityEvent Department Employee EmployeeStatus FkReftable1 ];
111+
112+
match_entry( 'Association',
113+
[ qw( Employee employee 1 emp_id emp_id ) ],
114+
[ qw( Activity activities * supervisor emp_id ) ],
115+
'Merged Association',
116+
);
117+
118+
119+
match_entry( 'Association',
120+
[ qw( Department department 1 dpt_id ) ],
121+
[ qw( Activity activities * dpt_id ) ],
122+
'Association: Department, Activity',
123+
);
124+
125+
126+
match_entry( 'Composition',
127+
[ qw( Activity activity 1 act_id ) ],
128+
[ qw( ActivityEvent activity_events * act_id ) ],
129+
'Composition Activity, ActivityEvent'
130+
);
131+
132+
match_entry( 'Association',
133+
[ qw( Employee employee 1 emp_id ) ],
134+
[ qw( EmployeeStatus employee_statuses * emp_id ) ],
135+
'Association: Employee, EmployeeStatus',
136+
);
137+
138+
match_entry( 'Association',
139+
[ qw( EmployeeStatus employee_status 1 emp_id_status ) ],
140+
[ qw( FkReftable1 fk_reftable_1s * emp_id_status ) ],
141+
'Association: two foreign keys to one reftable, one cascades',
142+
);
143+
144+
# checks for duplicate role as well.
145+
match_entry( 'Composition',
146+
[ qw( EmployeeStatus employee_status_2 1 emp_id ) ],
147+
[ qw( FkReftable1 fk_reftable_1s_2 * emp_id ) ],
148+
'Composition: two foreign keys to one reftable, one cascades',
149+
);
150+
151+
match_entry( 'Composition',
152+
[ qw( EmployeeStatus employee_status 1 emp_id emp_id_status ) ],
153+
[ qw( FkReftable2 fk_reftable_2s * emp_id emp_id_status ) ],
154+
'Merged Composition: two foreign keys to one reftable, both cascade',
155+
);
156+
157+
match_entry( 'Association',
158+
[ qw( EmployeeStatus employee_status 1 emp_id_status ) ],
159+
[ qw( FkReftable3 fk_reftable_3s * emp_id_status ) ],
160+
'Forced Association: two foreign keys to two reftables, both cascade (1)',
161+
);
162+
163+
164+
match_entry( 'Association',
165+
[ qw( Employee employee 1 emp_id ) ],
166+
[ qw( FkReftable3 fk_reftable_3s * emp_id ) ],
167+
'Forced Association: two foreign keys to two reftables, both cascade (2)',
168+
);
169+
62170

63171
# diag($perl_code);
64172

@@ -78,8 +186,8 @@ SKIP: {
78186
);
79187
$generator->parse_DBI($dbh);
80188
$perl_code = $generator->perl_code;
81-
like($perl_code, qr{Table\(qw/Foo}, "Table foo");
82-
like($perl_code, qr{Table\(qw/Bar}, "Table bar");
189+
like($perl_code, qr{Table\(qw/Foo}, "created Table foo");
190+
like($perl_code, qr{Table\(qw/Bar}, "created Table bar");
83191
}
84192

85193

0 commit comments

Comments
 (0)