ensembl-hive  2.5
BaseAdaptor.pm
Go to the documentation of this file.
1 =pod
2 
3 =head1 NAME
4 
6 
7 =head1 DESCRIPTION
8 
9  The base class for all other Object- or NakedTable- adaptors.
10  Performs the low-level SQL needed to retrieve and store data in tables.
11 
12 =head1 EXTERNAL DEPENDENCIES
13 
14  DBI 1.6
15 
16 =head1 LICENSE
17 
18  Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
19  Copyright [2016-2022] EMBL-European Bioinformatics Institute
20 
21  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
22  You may obtain a copy of the License at
23 
24  http://www.apache.org/licenses/LICENSE-2.0
25 
26  Unless required by applicable law or agreed to in writing, software distributed under the License
27  is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28  See the License for the specific language governing permissions and limitations under the License.
29 
30 =head1 CONTACT
31 
32  Please subscribe to the Hive mailing list: http://listserver.ebi.ac.uk/mailman/listinfo/ehive-users to discuss Hive-related questions or to be notified of our updates
33 
34 =cut
35 
36 
37 package Bio::EnsEMBL::Hive::DBSQL::BaseAdaptor;
38 
39 use strict;
40 use warnings;
41 no strict 'refs'; # needed to allow AUTOLOAD create new methods
42 use DBI 1.6; # the 1.6 functionality is important for detecting autoincrement fields and other magic.
43 
44 use Bio::EnsEMBL::Hive::Utils ('stringify', 'throw');
45 
46 
47 sub default_table_name {
48  throw("Please define table_name either by setting it via table_name() method or by redefining default_table_name() in your adaptor class");
49 }
50 
51 
52 sub default_insertion_method {
53  return 'INSERT';
54 }
55 
56 
57 sub default_overflow_limit {
58  return {
59  # 'overflow_column1_name' => column1_size,
60  # 'overflow_column2_name' => column2_size,
61  # ...
62  };
63 }
64 
65 sub default_input_column_mapping {
66  return {
67  # 'original_column1' => "original_column1*10 AS c1_times_ten",
68  # 'original_column2' => "original_column2+1 AS c2_plus_one",
69  # ...
70  };
71 }
72 
73 sub do_not_update_columns {
74  return [];
75 }
76 
77 # ---------------------------------------------------------------------------
78 
79 sub new {
80  my $class = shift @_;
81  my $dbobj = shift @_;
82 
83  my $self = bless {}, $class;
84 
85  if ( !defined $dbobj || !ref $dbobj ) {
86  throw("Don't have a db [$dbobj] for new adaptor");
87  }
88 
89  if ( ref($dbobj) =~ /DBConnection$/ ) {
90  $self->dbc($dbobj);
91  } elsif( UNIVERSAL::can($dbobj, 'dbc') ) {
92  $self->dbc( $dbobj->dbc );
93  $self->db( $dbobj );
94  } else {
95  throw("I was given [$dbobj] for a new adaptor");
96  }
97 
98  my %options = @_;
99 
100  foreach my $option_name (keys %options) {
101  if( UNIVERSAL::can( $self, $option_name ) ) {
102  if(defined(my $option_value = delete $options{ $option_name })) {
103  $self->$option_name( $option_value );
104  }
105  }
106  }
107 
108  return $self;
109 }
110 
111 
112 sub db {
113  my $self = shift @_;
114 
115  if(@_) { # setter
116  $self->{_db} = shift @_;
117  }
118  return $self->{_db};
119 }
120 
121 
122 sub dbc {
123  my $self = shift @_;
124 
125  if(@_) { # setter
126  $self->{_dbc} = shift @_;
127  }
128  return $self->{_dbc};
129 }
130 
131 
132 sub prepare {
133  my ( $self, $sql ) = @_;
134 
135  # Uncomment next line to cancel caching on the SQL side.
136  # Needed for timing comparisons etc.
137  #$sql =~ s/SELECT/SELECT SQL_NO_CACHE/i;
138 
139  return $self->dbc->prepare($sql);
140 }
141 
142 
143 sub overflow_limit {
144  my $self = shift @_;
145 
146  if(@_) { # setter
147  $self->{_overflow_limit} = shift @_;
148  }
149  return $self->{_overflow_limit} || $self->default_overflow_limit();
150 }
151 
152 
153 sub input_column_mapping {
154  my $self = shift @_;
155 
156  if(@_) { # setter
157  $self->{_input_column_mapping} = shift @_;
158  }
159  return $self->{_input_column_mapping} || $self->default_input_column_mapping();
160 }
161 
162 
163 sub table_name {
164  my $self = shift @_;
165 
166  if(@_) { # setter
167  $self->{_table_name} = shift @_;
168  $self->_table_info_loader();
169  }
170  return $self->{_table_name} || $self->default_table_name();
171 }
172 
173 
174 sub insertion_method {
175  my $self = shift @_;
176 
177  if(@_) { # setter
178  $self->{_insertion_method} = shift @_;
179  }
180  return $self->{_insertion_method} || $self->default_insertion_method();
181 }
182 
183 
184 sub column_set {
185  my $self = shift @_;
186 
187  if(@_) { # setter
188  $self->{_column_set} = shift @_;
189  } elsif( !defined( $self->{_column_set} ) ) {
190  $self->_table_info_loader();
191  }
192  return $self->{_column_set};
193 }
194 
195 
196 sub primary_key { # not necessarily auto-incrementing
197  my $self = shift @_;
198 
199  if(@_) { # setter
200  $self->{_primary_key} = shift @_;
201  } elsif( !defined( $self->{_primary_key} ) ) {
202  $self->_table_info_loader();
203  }
204  return $self->{_primary_key};
205 }
206 
207 
208 sub updatable_column_list { # it's just a cashed view, you cannot set it directly
209  my $self = shift @_;
210 
211  unless($self->{_updatable_column_list}) {
212  my %primary_key_set = map { $_ => 1 } @{$self->primary_key};
213  my %non_updatable_set = map { $_ => 1 } @{$self->do_not_update_columns};
214  my $column_set = $self->column_set();
215  $self->{_updatable_column_list} = [ grep { not ($primary_key_set{$_} || $non_updatable_set{$_}) } keys %$column_set ];
216  }
217  return $self->{_updatable_column_list};
218 }
219 
220 
221 sub autoinc_id {
222  my $self = shift @_;
223 
224  if(@_) { # setter
225  $self->{_autoinc_id} = shift @_;
226  } elsif( !defined( $self->{_autoinc_id} ) ) {
227  $self->_table_info_loader();
228  }
229  return $self->{_autoinc_id};
230 }
231 
232 
233 sub _table_info_loader {
234  my $self = shift @_;
235 
236  my $dbc = $self->dbc();
237  my $driver = $dbc->driver();
238  my $dbname = $dbc->dbname();
239  my $table_name = $self->table_name();
240 
241  my %column_set = ();
242  my $autoinc_id = '';
243  my @primary_key = $dbc->primary_key(undef, undef, $table_name);
244 
245  my $sth = $dbc->column_info(undef, undef, $table_name, '%');
246  $sth->execute();
247  while (my $row = $sth->fetchrow_hashref()) {
248  my ( $column_name, $column_type ) = @$row{'COLUMN_NAME', 'TYPE_NAME'};
249 
250  # warn "ColumnInfo [$table_name/$column_name] = $column_type\n";
251 
252  $column_set{$column_name} = $column_type;
253 
254  if( ($column_name eq $table_name.'_id')
255  or ($table_name eq 'analysis_base' and $column_name eq 'analysis_id') ) { # a special case (historical)
256  $autoinc_id = $column_name;
257  }
258  }
259  $sth->finish;
260 
261  $self->column_set( \%column_set );
262  $self->primary_key( \@primary_key );
263  $self->autoinc_id( $autoinc_id );
264 }
265 
266 
267 sub count_all {
268  my ($self, $constraint, $key_list, @bind_values) = @_;
269 
270  my $table_name = $self->table_name();
271  my $driver = $self->dbc->driver();
272  my $count_col_name = $driver eq 'pgsql' ? 'count' : 'COUNT(*)';
273 
274  my $sql = "SELECT ".($key_list ? join(', ', @$key_list, '') : '')."COUNT(*) FROM $table_name";
275 
276  if($constraint) {
277  # in case $constraint contains any kind of JOIN (regular, LEFT, RIGHT, etc) do not put WHERE in front:
278  $sql .= (($constraint=~/\bJOIN\b/i) ? ' ' : ' WHERE ') . $constraint;
279  }
280 
281  if($key_list) {
282  $sql .= " GROUP BY ".join(', ', @$key_list);
283  }
284  # warn "SQL: $sql\n";
285 
286  my $sth = $self->prepare($sql);
287  $sth->execute(@bind_values);
288 
289  my $result_struct; # will be autovivified to the correct data structure
290 
291  while(my $hashref = $sth->fetchrow_hashref) {
292 
293  my $pptr = \$result_struct;
294  if($key_list) {
295  foreach my $syll (@$key_list) {
296  $pptr = \$$pptr->{$hashref->{$syll}}; # using pointer-to-pointer to enforce same-level vivification
297  }
298  }
299  $$pptr = $hashref->{$count_col_name};
300  }
301 
302  unless(defined($result_struct)) {
303  if($key_list and scalar(@$key_list)) {
304  $result_struct = {};
305  } else {
306  $result_struct = 0;
307  }
308  }
309 
310  return $result_struct;
311 }
312 
313 
314 sub fetch_all {
315  my ($self, $constraint, $one_per_key, $key_list, $value_column) = @_;
316 
317  my $table_name = $self->table_name();
318  my $input_column_mapping = $self->input_column_mapping();
319 
320  my $sql = 'SELECT ' . join(', ', map { $input_column_mapping->{$_} // "$table_name.$_" } keys %{$self->column_set()}) . " FROM $table_name";
321 
322  if($constraint) {
323  # in case $constraint contains any kind of JOIN (regular, LEFT, RIGHT, etc) do not put WHERE in front:
324  $sql .= (($constraint=~/\bJOIN\b/i or $constraint=~/^LIMIT|ORDER|GROUP/) ? ' ' : ' WHERE ') . $constraint;
325  }
326 
327  # warn "SQL: $sql\n";
328 
329  my $sth = $self->prepare($sql);
330  $sth->execute;
331 
332  my @overflow_columns = keys %{ $self->overflow_limit() };
333  my $overflow_adaptor = scalar(@overflow_columns) && $self->db->get_AnalysisDataAdaptor();
334 
335  my $result_struct; # will be autovivified to the correct data structure
336 
337  while(my $hashref = $sth->fetchrow_hashref) {
338 
339  foreach my $overflow_key (@overflow_columns) {
340  if($hashref->{$overflow_key} =~ /^_ext(?:\w+)_data_id (\d+)$/) {
341  $hashref->{$overflow_key} = $overflow_adaptor->fetch_by_analysis_data_id_TO_data($1);
342  }
343  }
344 
345  my $pptr = \$result_struct;
346  if($key_list) {
347  foreach my $syll (@$key_list) {
348  $pptr = \$$pptr->{$hashref->{$syll}}; # using pointer-to-pointer to enforce same-level vivification
349  }
350  }
351  my $object = $value_column
352  ? ( (ref($value_column) eq 'ARRAY')
353  ? { map { ($_ => $hashref->{$_}) } @$value_column } # project to a subhash
354  : $hashref->{$value_column} # project to just one field
355  )
356  : $self->objectify($hashref); # keep the whole object
357 
358  if($one_per_key) {
359  $$pptr = $object; # just return the one value (either the key_list is unique or override)
360  } else {
361  push @$$pptr, $object; # return a list of values that potentially share the same key_list
362  }
363  }
364  $sth->finish;
365 
366  unless(defined($result_struct)) {
367  if($key_list and scalar(@$key_list)) {
368  $result_struct = {};
369  } elsif(!$one_per_key) {
370  $result_struct = [];
371  }
372  }
373 
374  return $result_struct; # either listref or hashref is returned, depending on the call parameters
375 }
376 
377 
378 sub primary_key_constraint {
379  my $self = shift @_;
380  my $sliceref = shift @_;
381 
382  my $primary_key = $self->primary_key(); # Attention: the order of primary_key columns of your call should match the order in the table definition!
383 
384  if(@$primary_key) {
385  return join (' AND ', map { $primary_key->[$_]."='".$sliceref->[$_]."'" } (0..scalar(@$primary_key)-1));
386  } else {
387  my $table_name = $self->table_name();
388  throw("Table '$table_name' doesn't have a primary_key");
389  }
390 }
391 
392 
393 sub fetch_by_dbID {
394  my $self = shift @_; # the rest in @_ should be primary_key column values
395 
396  return $self->fetch_all( $self->primary_key_constraint( \@_ ), 1 );
397 }
398 
399 
400 sub remove_all { # remove entries by a constraint
401  my $self = shift @_;
402  my $constraint = shift @_ || 1;
403 
404  my $table_name = $self->table_name();
405 
406  my $sql = "DELETE FROM $table_name WHERE $constraint";
407  my $sth = $self->prepare($sql);
408  $sth->execute();
409  $sth->finish();
410 }
411 
412 
413 sub remove { # remove the object by primary_key
414  my $self = shift @_;
415  my $object = shift @_;
416 
417  # the object hasn't actually been stored yet / in this database
418  return if(UNIVERSAL::can($object, 'adaptor') and (!$object->adaptor or $object->adaptor != $self));
419 
420  my $primary_key_constraint = $self->primary_key_constraint( $self->slicer($object, $self->primary_key()) );
421 
422  return $self->remove_all( $primary_key_constraint );
423 }
424 
425 
426 sub update { # update (some or all) non_primary columns from the primary
427  my $self = shift @_;
428  my $object = shift @_; # the rest in @_ should be the column names to be updated
429 
430  my $table_name = $self->table_name();
431  my $primary_key_constraint = $self->primary_key_constraint( $self->slicer($object, $self->primary_key()) );
432  my $columns_to_update = scalar(@_) ? \@_ : $self->updatable_column_list();
433  my $values_to_update = $self->slicer( $object, $columns_to_update );
434 
435  unless(@$columns_to_update) {
436  throw("There are no dependent columns to update, as everything seems to belong to the primary key");
437  }
438 
439  my @placeholders = ();
440  my @values = ();
441  foreach my $idx (0..scalar(@$columns_to_update)-1) {
442  my ($column_name, $value) = ($columns_to_update->[$idx], $values_to_update->[$idx]);
443 
444  if($column_name =~ /^when_/ and defined($value) and $value eq 'CURRENT_TIMESTAMP') {
445  push @placeholders, $column_name.'=CURRENT_TIMESTAMP';
446  } else {
447  push @placeholders, $column_name.'=?';
448  push @values, $value;
449  }
450  }
451 
452  my $sql = "UPDATE $table_name SET ".join(', ', @placeholders)." WHERE $primary_key_constraint";
453  # warn "SQL: $sql\n";
454  my $sth = $self->prepare($sql);
455  # warn "VALUES_TO_UPDATE: ".join(', ', map { "'$_'" } @values)."\n";
456  $sth->execute( @values);
457 
458  $sth->finish();
459 }
460 
461 
462 sub store_or_update_one {
463  my ($self, $object, $filter_columns) = @_;
464 
465  #use Data::Dumper;
466  if(UNIVERSAL::can($object, 'adaptor') and $object->adaptor and $object->adaptor==$self) { # looks like it has been previously stored
467  if( @{ $self->primary_key() } and @{ $self->updatable_column_list() } ) {
468  $self->update( $object );
469  #warn "store_or_update_one: updated [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
470  } else {
471  #warn "store_or_update_one: non-updatable [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
472  }
473  } elsif( my $present = $self->check_object_present_in_db_by_content( $object, $filter_columns ) ) {
474  $self->mark_stored($object, $present);
475  #warn "store_or_update_one: found [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."] in db by content of (".join(', ', @$filter_columns).")\n";
476  if( @{ $self->primary_key() } and @{ $self->updatable_column_list() } ) {
477  #warn "store_or_update_one: updating the columns (".join(', ', @{ $self->updatable_column_list() }).")\n";
478  $self->update( $object );
479  }
480  } else {
481  $self->store( $object );
482  #warn "store_or_update_one: stored [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
483  }
484 }
485 
486 
487 sub check_object_present_in_db_by_content { # return autoinc_id/undef if the table has autoinc_id or just 1/undef if not
488  my ( $self, $object, $filter_columns ) = @_;
489 
490  my $table_name = $self->table_name();
491  my $column_set = $self->column_set();
492  my $autoinc_id = $self->autoinc_id();
493 
494  if($filter_columns) {
495  # make sure all fields exist in the database as columns:
496  $filter_columns = [ map { $column_set->{$_} ? $_ : $_.'_id' } @$filter_columns ];
497  } else {
498  # we look for identical contents, so must skip the autoinc_id columns when fetching:
499  $filter_columns = [ grep { $_ ne $autoinc_id } keys %$column_set ];
500  }
501  my %filter_hash;
502  @filter_hash{ @$filter_columns } = @{ $self->slicer( $object, $filter_columns ) };
503 
504  my @constraints = ();
505  my @values = ();
506  while(my ($column, $value) = each %filter_hash ) {
507  if( defined($value) ) {
508  push @constraints, "$column = ?";
509  push @values, $value;
510  } else {
511  push @constraints, "$column IS NULL";
512  }
513  }
514 
515  my $sql = 'SELECT '.($autoinc_id or 1)." FROM $table_name WHERE ". join(' AND ', @constraints);
516  my $sth = $self->prepare( $sql );
517  $sth->execute( @values );
518 
519  my ($return_value) = $sth->fetchrow_array();
520 #warn "check_object_present_in_db_by_content: sql= $sql WITH VALUES (".join(', ', @values).") ---> return_value=".($return_value//'undef')."\n";
521  $sth->finish;
522 
523  return $return_value;
524 }
525 
526 
527 sub class_specific_execute {
528  my ($self, $object, $sth, $values) = @_;
529 
530  my $return_code = $sth->execute( @$values );
531 
532  return $return_code;
533 }
534 
535 
536 sub store {
537  my ($self, $object_or_list) = @_;
538 
539  my $objects = (ref($object_or_list) eq 'ARRAY') # ensure we get an array of objects to store
540  ? $object_or_list
541  : [ $object_or_list ];
542  return ([], 0) unless(scalar(@$objects));
543 
544  my $table_name = $self->table_name();
545  my $autoinc_id = $self->autoinc_id();
546  my $all_storable_columns = [ grep { $_ ne $autoinc_id } keys %{ $self->column_set() } ];
547  my $driver = $self->dbc->driver();
548  my $insertion_method = $self->insertion_method; # INSERT, INSERT_IGNORE or REPLACE
549  $insertion_method =~ s/_/ /g;
550  if($driver eq 'sqlite') {
551  $insertion_method =~ s/INSERT IGNORE/INSERT OR IGNORE/ig;
552  } elsif($driver eq 'pgsql') {
553  # Rules have been created to mimic the behaviour INSERT IGNORE / REPLACE
554  # Here we can do fall-back to a standard INSERT
555  $insertion_method = 'INSERT';
556  }
557 
558  my %hashed_sth = (); # do not prepare statements until there is a real need
559 
560  my $stored_this_time = 0;
561 
562  foreach my $object (@$objects) {
563  my ($columns_being_stored, $column_key) = $self->keys_to_columns($object);
564  # warn "COLUMN_KEY='$column_key'\n";
565 
566  my $this_sth;
567 
568  # only prepare (once!) if we get here:
569  unless($this_sth = $hashed_sth{$column_key}) {
570  # By using question marks we can insert true NULLs by setting corresponding values to undefs:
571  my $sql = "$insertion_method INTO $table_name (".join(', ', @$columns_being_stored).') VALUES ('.join(',', (('?') x scalar(@$columns_being_stored))).')';
572  # warn "STORE: $sql\n";
573  $this_sth = $hashed_sth{$column_key} = $self->prepare( $sql ) or throw("Could not prepare statement: $sql");
574  }
575 
576  # warn "STORED_COLUMNS: ".stringify($columns_being_stored)."\n";
577  my $values_being_stored = $self->slicer( $object, $columns_being_stored );
578  # warn "STORED_VALUES: ".stringify($values_being_stored)."\n";
579 
580  my $return_code = $self->class_specific_execute($object, $this_sth, $values_being_stored )
581  # using $return_code in boolean context allows to skip the value '0E0' ('no rows affected') that Perl treats as zero but regards as true:
582  or throw("Could not store fields\n\t{$column_key}\nwith data:\n\t(".join(',', @$values_being_stored).')');
583 
584  if($return_code > 0) { # <--- for the same reason we have to be explicitly numeric here
585  # FIXME: does this work if the "MySQL server has gone away" ?
586  my $liid = $autoinc_id && $self->dbc->db_handle->last_insert_id(undef, undef, $table_name, $autoinc_id);
587  $self->mark_stored($object, $liid );
588  ++$stored_this_time;
589  }
590  }
591 
592  foreach my $sth (values %hashed_sth) {
593  $sth->finish();
594  }
595 
596  return ($object_or_list, $stored_this_time);
597 }
598 
599 
600 sub _multi_column_filter {
601  my ($self, $filter_string, $filter_values, $column_set) = @_;
602 
603  # NB: this filtering happens BEFORE any possible overflow via analysis_data, so will not be done on overflow_columns
604  my $filter_components = $filter_string && [ split(/_AND_/i, $filter_string) ];
605  if($filter_components) {
606  foreach my $column_name ( @$filter_components ) {
607  unless($column_set->{$column_name}) {
608  throw("unknown column '$column_name'");
609  }
610  }
611  }
612 
613  my $filter_sql = $filter_components && join(' AND ', map { defined($filter_values->[$_]) ? "$filter_components->[$_]='$filter_values->[$_]'" : $filter_components->[$_].' IS NULL' } 0..scalar(@$filter_components)-1);
614 
615  return $filter_sql;
616 }
617 
618 
619 sub DESTROY { } # to simplify AUTOLOAD
620 
621 sub AUTOLOAD {
622  our $AUTOLOAD;
623 
624  if($AUTOLOAD =~ /::fetch(_all)?(?:_by_(\w+?))?(?:_HASHED_FROM_(\w+?))?(?:_TO_(\w+?))?$/) {
625  my $all = $1;
626  my $filter_string = $2;
627  my $key_string = $3;
628  my $value_column = $4;
629 
630  my ($self) = @_;
631  my $column_set = $self->column_set();
632 
633  my $key_components = $key_string && [ split(/_AND_/i, $key_string) ];
634  if($key_components) {
635  foreach my $column_name ( @$key_components ) {
636  unless($column_set->{$column_name}) {
637  throw("unknown column '$column_name'");
638  }
639  }
640  }
641 
642  if($value_column && !$column_set->{$value_column}) {
643  throw("unknown column '$value_column'");
644  }
645 
646 # warn "Setting up '$AUTOLOAD' method\n";
647  *$AUTOLOAD = sub {
648  my $self = shift @_;
649  return $self->fetch_all(
650  $self->_multi_column_filter($filter_string, \@_, $column_set),
651  !$all,
652  $key_components,
653  $value_column
654  );
655  };
656  goto &$AUTOLOAD; # restart the new method
657 
658  } elsif($AUTOLOAD =~ /::count_all(?:_by_(\w+?))?(?:_HASHED_FROM_(\w+?))?$/) {
659  my $filter_string = $1;
660  my $key_string = $2;
661 
662  my ($self) = @_;
663  my $column_set = $self->column_set();
664 
665  my $key_components = $key_string && [ split(/_AND_/i, $key_string) ];
666  if($key_components) {
667  foreach my $column_name ( @$key_components ) {
668  unless($column_set->{$column_name}) {
669  throw("unknown column '$column_name'");
670  }
671  }
672  }
673 
674 # warn "Setting up '$AUTOLOAD' method\n";
675  *$AUTOLOAD = sub {
676  my $self = shift @_;
677  return $self->count_all(
678  $self->_multi_column_filter($filter_string, \@_, $column_set),
679  $key_components,
680  );
681  };
682  goto &$AUTOLOAD; # restart the new method
683 
684  } elsif($AUTOLOAD =~ /::remove_all_by_(\w+)$/) {
685  my $filter_string = $1;
686 
687  my ($self) = @_;
688  my $column_set = $self->column_set();
689 
690 # warn "Setting up '$AUTOLOAD' method\n";
691  *$AUTOLOAD = sub {
692  my $self = shift @_;
693  return $self->remove_all(
694  $self->_multi_column_filter($filter_string, \@_, $column_set),
695  );
696  };
697  goto &$AUTOLOAD; # restart the new method
698 
699  } elsif($AUTOLOAD =~ /::update_(\w+)$/) {
700  my @columns_to_update = split(/_AND_/i, $1);
701 # warn "Setting up '$AUTOLOAD' method\n";
702  *$AUTOLOAD = sub { my ($self, $object) = @_; return $self->update($object, @columns_to_update); };
703  goto &$AUTOLOAD; # restart the new method
704  } else {
705  warn "sub '$AUTOLOAD' not implemented";
706  }
707 }
708 
709 1;
710