Factor list attributes into variant role
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Schema / Constraint.pm
CommitLineData
3c5de62a 1package SQL::Translator::Schema::Constraint;
2
3c5de62a 3=pod
4
5=head1 NAME
6
7SQL::Translator::Schema::Constraint - SQL::Translator constraint object
8
9=head1 SYNOPSIS
10
11 use SQL::Translator::Schema::Constraint;
12 my $constraint = SQL::Translator::Schema::Constraint->new(
13 name => 'foo',
14 fields => [ id ],
43b9dc7a 15 type => PRIMARY_KEY,
3c5de62a 16 );
17
18=head1 DESCRIPTION
19
20C<SQL::Translator::Schema::Constraint> is the constraint object.
21
22=head1 METHODS
23
24=cut
25
96a5759e 26use Moo;
43b9dc7a 27use SQL::Translator::Schema::Constants;
0fb58589 28use SQL::Translator::Utils qw(ex2err throw);
29use SQL::Translator::Role::ListAttr;
96a5759e 30use SQL::Translator::Types qw(schema_obj);
96a5759e 31
954ed12e 32extends 'SQL::Translator::Schema::Object';
b6a880d1 33
0c04c5a2 34our $VERSION = '1.59';
3c5de62a 35
695c2da2 36my %VALID_CONSTRAINT_TYPE = (
43b9dc7a 37 PRIMARY_KEY, 1,
38 UNIQUE, 1,
39 CHECK_C, 1,
40 FOREIGN_KEY, 1,
695c2da2 41 NOT_NULL, 1,
42);
3c5de62a 43
3c5de62a 44=head2 new
45
46Object constructor.
47
48 my $schema = SQL::Translator::Schema::Constraint->new(
dedb8f3b 49 table => $table, # table to which it belongs
3c5de62a 50 type => 'foreign_key', # type of table constraint
dedb8f3b 51 name => 'fk_phone_id', # name of the constraint
52 fields => 'phone_id', # field in the referring table
65157eda 53 reference_fields => 'phone_id', # referenced field
54 reference_table => 'phone', # referenced table
3c5de62a 55 match_type => 'full', # how to match
2d034ab4 56 on_delete => 'cascade', # what to do on deletes
57 on_update => '', # what to do on updates
3c5de62a 58 );
59
60=cut
61
96a5759e 62# Override to remove empty arrays from args.
63# t/14postgres-parser breaks without this.
64around BUILDARGS => sub {
65 my $orig = shift;
9371be50 66 my $self = shift;
96a5759e 67 my $args = $self->$orig(@_);
43b9dc7a 68
96a5759e 69 foreach my $arg (keys %{$args}) {
46ad748f 70 delete $args->{$arg} if ref($args->{$arg}) eq "ARRAY" && !@{$args->{$arg}};
96a5759e 71 }
72 if (exists $args->{fields}) {
73 $args->{field_names} = delete $args->{fields};
74 }
75 return $args;
76};
43b9dc7a 77
78=head2 deferrable
79
2d034ab4 80Get or set whether the constraint is deferrable. If not defined,
43b9dc7a 81then returns "1." The argument is evaluated by Perl for True or
82False, so the following are eqivalent:
83
84 $deferrable = $field->deferrable(0);
85 $deferrable = $field->deferrable('');
86 $deferrable = $field->deferrable('0');
87
88=cut
89
96a5759e 90has deferrable => ( is => 'rw', coerce => sub { $_[0] ? 1 : 0 }, default => sub { 1 } );
43b9dc7a 91
92=head2 expression
93
94Gets and set the expression used in a CHECK constraint.
95
96 my $expression = $constraint->expression('...');
97
98=cut
99
96a5759e 100has expression => ( is => 'rw', default => sub { '' } );
43b9dc7a 101
96a5759e 102around expression => sub {
103 my ($orig, $self, $arg) = @_;
104 $self->$orig($arg || ());
105};
43b9dc7a 106
43b9dc7a 107sub is_valid {
108
109=pod
110
111=head2 is_valid
112
113Determine whether the constraint is valid or not.
114
115 my $ok = $constraint->is_valid;
116
117=cut
118
119 my $self = shift;
120 my $type = $self->type or return $self->error('No type');
121 my $table = $self->table or return $self->error('No table');
122 my @fields = $self->fields or return $self->error('No fields');
123 my $table_name = $table->name or return $self->error('No table name');
124
125 for my $f ( @fields ) {
126 next if $table->get_field( $f );
127 return $self->error(
128 "Constraint references non-existent field '$f' ",
129 "in table '$table_name'"
130 );
131 }
132
133 my $schema = $table->schema or return $self->error(
134 'Table ', $table->name, ' has no schema object'
135 );
136
137 if ( $type eq FOREIGN_KEY ) {
138 return $self->error('Only one field allowed for foreign key')
139 if scalar @fields > 1;
140
ea93df61 141 my $ref_table_name = $self->reference_table or
43b9dc7a 142 return $self->error('No reference table');
143
144 my $ref_table = $schema->get_table( $ref_table_name ) or
145 return $self->error("No table named '$ref_table_name' in schema");
146
147 my @ref_fields = $self->reference_fields or return;
148
149 return $self->error('Only one field allowed for foreign key reference')
150 if scalar @ref_fields > 1;
151
152 for my $ref_field ( @ref_fields ) {
153 next if $ref_table->get_field( $ref_field );
154 return $self->error(
96a5759e 155 "Constraint from field(s) ".
156 join(', ', map {qq['$table_name.$_']} @fields).
43b9dc7a 157 " to non-existent field '$ref_table_name.$ref_field'"
158 );
159 }
160 }
161 elsif ( $type eq CHECK_C ) {
ea93df61 162 return $self->error('No expression for CHECK') unless
43b9dc7a 163 $self->expression;
164 }
165
166 return 1;
167}
168
3c5de62a 169=head2 fields
170
43b9dc7a 171Gets and set the fields the constraint is on. Accepts a string, list or
172arrayref; returns an array or array reference. Will unique the field
173names and keep them in order by the first occurrence of a field name.
174
ac095e5e 175The fields are returned as Field objects if they exist or as plain
176names if not. (If you just want the names and want to avoid the Field's overload
177magic use L<field_names>).
178
179Returns undef or an empty list if the constraint has no fields set.
180
43b9dc7a 181 $constraint->fields('id');
182 $constraint->fields('id', 'name');
183 $constraint->fields( 'id, name' );
184 $constraint->fields( [ 'id', 'name' ] );
185 $constraint->fields( qw[ id name ] );
3c5de62a 186
43b9dc7a 187 my @fields = $constraint->fields;
3c5de62a 188
189=cut
190
96a5759e 191sub fields {
192 my $self = shift;
193 my $table = $self->table;
194 my @tables = map { $table->get_field($_) || $_ } @{$self->field_names(@_) || []};
195 return wantarray ? @tables
196 : @tables ? \@tables
197 : undef;
3c5de62a 198}
199
ac095e5e 200=head2 field_names
201
202Read-only method to return a list or array ref of the field names. Returns undef
10f70490 203or an empty list if the constraint has no fields set. Useful if you want to
ac095e5e 204avoid the overload magic of the Field objects returned by the fields method.
205
206 my @names = $constraint->field_names;
207
208=cut
209
0fb58589 210with ListAttr field_names => ( uniq => 1, undef_if_empty => 1 );
752608d5 211
212=head2 match_type
213
214Get or set the constraint's match_type. Only valid values are "full"
840447a5 215"partial" and "simple"
752608d5 216
217 my $match_type = $constraint->match_type('FULL');
218
219=cut
220
96a5759e 221has match_type => (
222 is => 'rw',
223 default => sub { '' },
224 coerce => sub { lc $_[0] },
225 isa => sub {
226 my $arg = $_[0];
227 throw("Invalid match type: $arg")
228 if $arg && !($arg eq 'full' || $arg eq 'partial' || $arg eq 'simple');
229 },
230);
752608d5 231
96a5759e 232around match_type => \&ex2err;
3c5de62a 233
234=head2 name
235
236Get or set the constraint's name.
237
238 my $name = $constraint->name('foo');
239
240=cut
241
96a5759e 242has name => ( is => 'rw', default => sub { '' } );
3c5de62a 243
96a5759e 244around name => sub {
245 my ($orig, $self, $arg) = @_;
246 $self->$orig($arg || ());
247};
dedb8f3b 248
249=head2 options
250
ea93df61 251Gets or adds to the constraints's options (e.g., "INITIALLY IMMEDIATE").
dedb8f3b 252Returns an array or array reference.
253
254 $constraint->options('NORELY');
255 my @options = $constraint->options;
256
257=cut
258
0fb58589 259with ListAttr options => ();
43b9dc7a 260
261=head2 on_delete
262
263Get or set the constraint's "on delete" action.
264
265 my $action = $constraint->on_delete('cascade');
266
267=cut
268
96a5759e 269has on_delete => ( is => 'rw', default => sub { '' } );
43b9dc7a 270
96a5759e 271around on_delete => sub {
272 my ($orig, $self, $arg) = @_;
273 $self->$orig($arg || ());
274};
43b9dc7a 275
276=head2 on_update
277
278Get or set the constraint's "on update" action.
279
280 my $action = $constraint->on_update('no action');
281
282=cut
283
96a5759e 284has on_update => ( is => 'rw', default => sub { '' } );
43b9dc7a 285
96a5759e 286around on_update => sub {
287 my ($orig, $self, $arg) = @_;
288 $self->$orig($arg || ());
289};
43b9dc7a 290
291=head2 reference_fields
292
293Gets and set the fields in the referred table. Accepts a string, list or
294arrayref; returns an array or array reference.
295
296 $constraint->reference_fields('id');
297 $constraint->reference_fields('id', 'name');
298 $constraint->reference_fields( 'id, name' );
299 $constraint->reference_fields( [ 'id', 'name' ] );
300 $constraint->reference_fields( qw[ id name ] );
301
302 my @reference_fields = $constraint->reference_fields;
303
304=cut
305
0fb58589 306with ListAttr reference_fields => (
307 may_throw => 1,
96a5759e 308 builder => 1,
309 lazy => 1,
310);
311
96a5759e 312sub _build_reference_fields {
313 my ($self) = @_;
43b9dc7a 314
96a5759e 315 my $table = $self->table or throw('No table');
316 my $schema = $table->schema or throw('No schema');
317 if ( my $ref_table_name = $self->reference_table ) {
318 my $ref_table = $schema->get_table( $ref_table_name ) or
319 throw("Can't find table '$ref_table_name'");
320
321 if ( my $constraint = $ref_table->primary_key ) {
322 return [ $constraint->fields ];
323 }
324 else {
325 throw(
326 'No reference fields defined and cannot find primary key in ',
327 "reference table '$ref_table_name'"
328 );
329 }
43b9dc7a 330 }
331}
332
43b9dc7a 333=head2 reference_table
334
335Get or set the table referred to by the constraint.
336
337 my $reference_table = $constraint->reference_table('foo');
338
339=cut
340
96a5759e 341has reference_table => ( is => 'rw', default => sub { '' } );
3c5de62a 342
43b9dc7a 343=head2 table
3c5de62a 344
2d034ab4 345Get or set the constraint's table object.
3c5de62a 346
43b9dc7a 347 my $table = $field->table;
3c5de62a 348
349=cut
350
a5bfeba8 351has table => ( is => 'rw', isa => schema_obj('Table'), weak_ref => 1 );
43b9dc7a 352
96a5759e 353around table => \&ex2err;
43b9dc7a 354
dedb8f3b 355=head2 type
43b9dc7a 356
dedb8f3b 357Get or set the constraint's type.
43b9dc7a 358
dedb8f3b 359 my $type = $constraint->type( PRIMARY_KEY );
43b9dc7a 360
361=cut
362
96a5759e 363has type => (
364 is => 'rw',
365 default => sub { '' },
366 isa => sub {
367 throw("Invalid constraint type: $_[0]")
368 if $_[0] && !$VALID_CONSTRAINT_TYPE{ $_[0] };
369 },
370 coerce => sub { (my $t = $_[0]) =~ s/_/ /g; uc $t },
371);
abf315bb 372
96a5759e 373around type => \&ex2err;
abf315bb 374
375=head2 equals
376
377Determines if this constraint is the same as another
378
379 my $isIdentical = $constraint1->equals( $constraint2 );
380
381=cut
382
96a5759e 383around equals => sub {
384 my $orig = shift;
abf315bb 385 my $self = shift;
386 my $other = shift;
387 my $case_insensitive = shift;
d990d84b 388 my $ignore_constraint_names = shift;
ea93df61 389
96a5759e 390 return 0 unless $self->$orig($other);
b8d24485 391 return 0 unless $self->type eq $other->type;
d990d84b 392 unless ($ignore_constraint_names) {
393 return 0 unless $case_insensitive ? uc($self->name) eq uc($other->name) : $self->name eq $other->name;
394 }
abf315bb 395 return 0 unless $self->deferrable eq $other->deferrable;
4598b71c 396 #return 0 unless $self->is_valid eq $other->is_valid;
abf315bb 397 return 0 unless $case_insensitive ? uc($self->table->name) eq uc($other->table->name)
ea93df61 398 : $self->table->name eq $other->table->name;
abf315bb 399 return 0 unless $self->expression eq $other->expression;
ea93df61 400
6a0f3000 401 # Check fields, regardless of order
ea93df61 402 my %otherFields = (); # create a hash of the other fields
6a0f3000 403 foreach my $otherField ($other->fields) {
ea93df61 404 $otherField = uc($otherField) if $case_insensitive;
405 $otherFields{$otherField} = 1;
6a0f3000 406 }
407 foreach my $selfField ($self->fields) { # check for self fields in hash
ea93df61 408 $selfField = uc($selfField) if $case_insensitive;
409 return 0 unless $otherFields{$selfField};
410 delete $otherFields{$selfField};
6a0f3000 411 }
412 # Check all other fields were accounted for
413 return 0 unless keys %otherFields == 0;
414
415 # Check reference fields, regardless of order
ea93df61 416 my %otherRefFields = (); # create a hash of the other reference fields
6a0f3000 417 foreach my $otherRefField ($other->reference_fields) {
ea93df61 418 $otherRefField = uc($otherRefField) if $case_insensitive;
419 $otherRefFields{$otherRefField} = 1;
6a0f3000 420 }
421 foreach my $selfRefField ($self->reference_fields) { # check for self reference fields in hash
ea93df61 422 $selfRefField = uc($selfRefField) if $case_insensitive;
423 return 0 unless $otherRefFields{$selfRefField};
424 delete $otherRefFields{$selfRefField};
6a0f3000 425 }
426 # Check all other reference fields were accounted for
427 return 0 unless keys %otherRefFields == 0;
428
b8d24485 429 return 0 unless $case_insensitive ? uc($self->reference_table) eq uc($other->reference_table) : $self->reference_table eq $other->reference_table;
abf315bb 430 return 0 unless $self->match_type eq $other->match_type;
431 return 0 unless $self->on_delete eq $other->on_delete;
432 return 0 unless $self->on_update eq $other->on_update;
4598b71c 433 return 0 unless $self->_compare_objects(scalar $self->options, scalar $other->options);
434 return 0 unless $self->_compare_objects(scalar $self->extra, scalar $other->extra);
abf315bb 435 return 1;
96a5759e 436};
abf315bb 437
96a5759e 438# Must come after all 'has' declarations
439around new => \&ex2err;
440
3c5de62a 4411;
442
3c5de62a 443=pod
444
445=head1 AUTHOR
446
c3b0b535 447Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
3c5de62a 448
449=cut