package SQL::DB::Row; use strict; use warnings; use Carp qw(croak); use Scalar::Util qw(refaddr); use constant ORIGINAL => 0; use constant MODIFIED => 1; use constant STATUS => 2; sub make_class_from { my $proto = shift; @_ || croak 'make_class_from() requires arguments'; my @methods; my @tablecolumns; my $arows = {}; my $i = 0; foreach my $obj (@_) { my $method; my $set_method; if (UNIVERSAL::can($obj, '_column')) { # AColumn ($method = $obj->_as) =~ s/(^t\d+\.)|(^\w+\d+\.)//go; $set_method = 'set_'. $method; push(@methods, [$method, $set_method, $obj->_column]); push(@tablecolumns, $obj->_column->table->name .'.'. $obj->_column->name); push(@{$arows->{refaddr($obj->_arow)}}, [$i,$obj->_column]); } elsif (UNIVERSAL::can($obj, 'table')) { # Column $method = $obj->name; $set_method = 'set_'. $method; push(@methods, [$method, $set_method, $obj]); push(@tablecolumns, $obj->table->name .'.'. $obj->name); } elsif (UNIVERSAL::can($obj, '_as')) { # Expr $method = $obj->_as || $obj->as_string; $set_method = 'set_'. $method; push(@methods, [$method, $set_method, undef]); push(@tablecolumns, $method); } else { croak 'MultiRow takes AColumns, Columns or Exprs: '. ref($obj); } $i++; } my $class = $proto .'::'. join('_', @tablecolumns); no strict 'refs'; my $isa = \@{$class . '::ISA'}; if (defined @{$isa}) { return $class; } push(@{$isa}, $proto); ${$class.'::_arow_groups'} = [values %$arows] if(keys %$arows);; my $defaults = {}; $i = 0; foreach my $def (@methods) { # index/position in array ${$class.'::_index_'.$def->[0]} = $i; push(@{$class.'::_columns'}, $def->[2]); if (UNIVERSAL::isa($def->[2], 'SQL::DB::Schema::Column')) { if ($def->[2]->inflate) { push(@{$class.'::_inflate_indexes'}, $i); *{$class.'::_inflate'.$i} = $def->[2]->inflate; } if ($def->[2]->deflate) { push(@{$class.'::_deflate_indexes'}, $i); *{$class.'::_deflate'.$i} = $def->[2]->deflate; } if (defined $def->[2]->default) { $defaults->{$def->[0]} = $def->[2]->default; } } if (defined(&{$class.'::'.$def->[0]})) { croak 'Attempt to define column "'.$def->[0].'" twice. ' .'You cannot fetch two columns with the same name'; } push(@{$class.'::_column_names'}, $def->[0]); # accessor *{$class.'::'.$def->[0]} = sub { my $self = shift; my $pos = ${$class.'::_index_'.$def->[0]}; return $self->[$self->[STATUS]->[$pos]]->[$pos]; }; # modifier if ($def->[1]) { if (defined(&{$class.'::'.$def->[1]})) { die "double definition"; } if ($def->[2]) { # we have a column - record the modification *{$class.'::'.$def->[1]} = sub { my $self = shift; if (!@_) { croak $def->[1] . ' requires an argument'; } my $pos = ${$class.'::_index_'.$def->[0]}; $self->[STATUS]->[$pos] = 1; $self->[MODIFIED]->[$pos] = shift; return; }; } else { # no column, just change the value *{$class.'::'.$def->[1]} = sub { my $self = shift; if (!@_) { croak $def->[1] . ' requires an argument'; } my $pos = ${$class.'::_index_'.$def->[0]}; $self->[ORIGINAL]->[$pos] = shift; return; }; } } $i++; } *{$class.'::new_from_arrayref'} = sub { my $proto = shift; my $finalclass = ref($proto) || $proto; my $valref = shift || croak 'new_from_arrayref requires ARRAYREF argument'; ref($valref) eq 'ARRAY' || croak 'new_from_arrayref requires ARRAYREF argument'; my $self = [ $valref, # ORIGINAL [], # MODIFIED [map {ORIGINAL} (1..scalar @methods)], # STATUS ]; bless($self, $finalclass); return $self; }; *{$class.'::new'} = sub { my $proto = shift; my $incoming; if (ref($_[0]) and ref($_[0]) eq 'HASH') { $incoming = shift; } else { $incoming = {@_}; } my @status = map {ORIGINAL} (1.. scalar @methods); # Set the default values my @original = (); my $hash = {}; map {$hash->{$_} = $defaults->{$_}} keys %$defaults; while (my ($key,$val) = each %$hash) { my $i = ${$class.'::_index_'.$key}; if (defined($i)) { if (ref($val) and ref($val) eq 'CODE') { $original[$i] = &$val; } else { $original[$i] = $val; } } } # Set the incoming values $hash = {}; my @modified = (); map {$hash->{$_} = $incoming->{$_}} keys %$incoming; while (my ($key,$val) = each %$hash) { my $i = ${$class.'::_index_'.$key}; if (defined($i)) { $status[$i] = MODIFIED; $modified[$i] = $val; } } my $self = [ \@original, # ORIGINAL \@modified, # MODIFIED \@status, # STATUS ]; my $finalclass = ref($proto) || $proto; bless($self, $finalclass); return $self; }; *{$class.'::_column_names'} = sub { my $self = shift; return @{$class.'::_column_names'}; }; *{$class.'::_hashref'} = sub { my $self = shift; my $hashref = {}; my $i = 0; foreach my $def (@methods) { $hashref->{$def->[0]} = $self->[$self->[STATUS]->[$i]]->[$i]; $i++; } return $hashref; }; *{$class.'::_modified'} = sub { my $self = shift; my $colname = shift || croak 'usage: _modified($colname)'; my $i = ${$class.'::_index_'.$colname}; defined($i) || croak 'Column "'.$colname.'" is not in '.ref($self); return $self->[STATUS]->[$i]; }; *{$class.'::_hashref_modified'} = sub { my $self = shift; my $hashref = {}; my $i = 0; foreach my $def (@methods) { if ($self->[STATUS]->[$i] == MODIFIED) { $hashref->{$def->[0]} = $self->[MODIFIED]->[$i]; } $i++; } return $hashref; }; *{$class.'::_inflate'} = sub { my $self = shift; foreach my $i (@{$class .'::_inflate_indexes'}) { my $inflate = *{$class .'::_inflate'.$i}; next unless(defined($self->[$self->[STATUS]->[$i]]->[$i])); $self->[$self->[STATUS]->[$i]]->[$i] = &$inflate($self->[$self->[STATUS]->[$i]]->[$i]); } return $self; }; *{$class.'::_deflate'} = sub { my $self = shift; foreach my $i (@{$class .'::_deflate_indexes'}) { my $deflate = *{$class .'::_deflate'.$i}; next unless(defined($self->[$self->[STATUS]->[$i]]->[$i])); $self->[$self->[STATUS]->[$i]]->[$i] = &$deflate($self->[$self->[STATUS]->[$i]]->[$i]); } return $self; }; *{$class.'::q_insert'} = sub { my $self = shift; $self->_deflate; my @cols = @{$class .'::_columns'}; my $arows = {}; my $columns = {}; my $values = {}; my $i = 0; foreach my $col (@cols) { next unless($col); my $status = $self->[STATUS]->[$i]; my $colname = $col->name; my $tname = $col->table->name; if (!exists($arows->{$tname})) { $arows->{$tname} = $col->table->arow(); $columns->{$tname} = []; $values->{$tname} = []; } push(@{$columns->{$tname}}, $arows->{$tname}->$colname); push(@{$values->{$tname}}, $self->[$status]->[$i]); $i++; } my @queries; foreach my $tname (keys %{$columns}) { next unless(@{$columns->{$tname}}); push(@queries, [ insert => $columns->{$tname}, values => $values->{$tname}, ]); } $self->_inflate; # To prevent circular references all AColumns 'weaken' the link # to their ARow. This method returns AColumns to the caller, but # is the only holder of a strong reference to the ARows belonging # to those AColumns. So if we didn't return the ARow as well then # AColumn->_arow is undefined and SQL::DB::Schema::Query barfs. return ($arows, @queries); }; *{$class.'::q_update'} = sub { my $self = shift; $self->_deflate; my @cols = @{$class .'::_columns'}; my $arows = {}; my $primary = {}; my $updates = {}; my $where = {}; if (my $_arow_groups = ${$class.'::_arow_groups'}) { my $groupid = 0; foreach my $group (@$_arow_groups) { foreach my $group_item (@$group) { my ($i,$col) = @$group_item; my $colname = $col->name; if (!exists($arows->{$groupid})) { $arows->{$groupid} = $col->table->arow(); $updates->{$groupid} = [], } if ($col->primary) { # NULL primary columns can't be used. if (!defined($self->[$self->[STATUS]->[$i]]->[$i])) { next; } $primary->{$groupid} = 1; if (!$where->{$groupid}) { $where->{$groupid} = ($arows->{$groupid}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } else { $where->{$groupid} = $where->{$groupid} & ($arows->{$groupid}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } } if ($self->[STATUS]->[$i] == MODIFIED) { push(@{$updates->{$groupid}}, $arows->{$groupid}->$colname->set( $self->[MODIFIED]->[$i]) ); } } $groupid++; } } else { my $i = 0; foreach my $col (@cols) { next unless($col); my $colname = $col->name; my $tname = $col->table->name; if (!exists($arows->{$tname})) { $arows->{$tname} = $col->table->arow(); $updates->{$tname} = [], } if ($col->primary) { # NULL primary columns can't be used. if (!defined($self->[$self->[STATUS]->[$i]]->[$i])) { $i++; next; } $primary->{$tname} = 1; if (!$where->{$tname}) { $where->{$tname} = ($arows->{$tname}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } else { $where->{$tname} = $where->{$tname} & ($arows->{$tname}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } } if ($self->[STATUS]->[$i] == MODIFIED) { push(@{$updates->{$tname}}, $arows->{$tname}->$colname->set($self->[MODIFIED]->[$i]) ); } $i++; } } my @queries; foreach my $table (keys %{$primary}) { next unless($where->{$table} and @{$updates->{$table}}); push(@queries, [ update => $updates->{$table}, ($where->{$table} ? (where => $where->{$table}) : ()), ]); } $self->_inflate; # To prevent circular references all AColumns 'weaken' the link # to their ARow. This method returns AColumns to the caller, but # is the only holder of a strong reference to the ARows belonging # to those AColumns. So if we didn't return the ARow as well then # AColumn->_arow is undefined and SQL::DB::Schema::Query barfs. return ($arows, @queries); }; *{$class.'::q_delete'} = sub { my $self = shift; $self->_deflate; my @cols = @{$class .'::_columns'}; my $arows = {}; my $where = {}; my $i = 0; foreach my $col (@cols) { next unless($col); my $colname = $col->name; my $tname = $col->table->name; if (!exists($arows->{$tname})) { $arows->{$tname} = $col->table->arow(); } if ($col->primary) { # NULL primary columns can't be used. if (!defined($self->[$self->[STATUS]->[$i]]->[$i])) { $i++; next; } if (!$where->{$tname}) { $where->{$tname} = ($arows->{$tname}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } else { $where->{$tname} = $where->{$tname} & ($arows->{$tname}->$colname == $self->[$self->[STATUS]->[$i]]->[$i]); } } $i++; } my @queries; foreach my $table (keys %{$arows}) { next unless($where->{$table}); push(@queries, [ delete => $arows->{$table}, where => $where->{$table}, ]); } $self->_inflate; # To prevent circular references all AColumns 'weaken' the link # to their ARow. This method returns AColumns to the caller, but # is the only holder of a strong reference to the ARows belonging # to those AColumns. So if we didn't return the ARow as well then # AColumn->_arow is undefined and SQL::DB::Schema::Query barfs. return ($arows, @queries); }; *{$class.'::quickdump'} = sub { my $self = shift; my $str; my $i = 0; foreach my $def (@methods) { my $key = $def->[0] .($self->[STATUS]->[$i] ? '[m]' : ''); my $val = $self->[$self->[STATUS]->[$i]]->[$i]; if (defined($val) and $val !~ /^[[:print:]]+$/) { $val = '*BINARY DATA*'; } else { $val = (defined($val) ? $val : 'NULL'); } $str .= sprintf("\%-12s = \%s\n", $key, $val); $i++; } return $str; }; return $class; } 1; __END__ =head1 NAME SQL::DB::Row - description =head1 SYNOPSIS use SQL::DB::Row; =head1 DESCRIPTION B is ... =head1 METHODS =head2 make_class_from =head2 new_from_arrayref Create a new object from values contained a reference to an ARRAY. The array values must be in the same order as the definition of the class. =head2 new =head2 _hashref Returns a reference to a hash containing the keys and values of the object. =head2 _inflate =head2 _deflate =head1 FILES =head1 SEE ALSO L =head1 AUTHOR Mark Lawrence Enomad@null.netE =head1 COPYRIGHT AND LICENSE Copyright (C) 2007,2008 Mark Lawrence This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. =cut # vim: set tabstop=4 expandtab: