package Tangram::Relational::Engine::Class; use strict; use Tangram::Schema; use vars qw(@ISA); @ISA = qw( Tangram::Schema::Node ); use Carp qw(confess); sub new { bless { }, shift; } sub fracture { my ($self) = @_; delete $self->{BASES}; delete $self->{SPECS}; } sub initialize { my ($self, $engine, $class, $mapping) = @_; ref($self->{CLASS} = $class) && UNIVERSAL::isa($class, "Tangram::Schema::Class") or confess "not class but $class"; $self->{MAPPING} = $mapping; $self->{BASES} = [ map { $engine->get_class_engine($_) } $class->get_bases() ]; $self->{SPECS} = [ map { $engine->get_class_engine($_) } $class->get_specs() ]; $self->{ID_COL} = $engine->{SCHEMA}{sql}{id_col}; } sub get_instance_select { my ($self, $engine) = @_; return $self->{INSTANCE_SELECT} ||= do { my $schema = $engine->{SCHEMA}; my $id_col = $schema->{sql}{id_col}; my $context = { engine => $engine, schema => $schema, layout1 => $engine->{layout1} }; my (@tables, %seen, @cols, $root); $self->for_composing ( sub { my ($part) = @_; $root ||= $part; $context->{class} = $part->{CLASS}; push @cols, ( map { my ($table, $col) = @$_; push @tables, $table unless $seen{$table}++; "$table.$col" } $part->{MAPPING}->get_import_cols($context) ); } ); unless (@tables) { # in case the class has absolutely no state at all... @cols = $id_col; @tables = $root->{MAPPING}->get_table; } my $first_table = shift @tables; sprintf("SELECT\n %s\nFROM\n %s\nWHERE\n %s", join(",\n ", @cols), join(",\n ", $first_table, @tables), join("\tAND\n ", "$first_table.$id_col = ?", (map { "$first_table.$id_col = $_.$id_col" } @tables) ) ); }; } sub get_insert_statements { my ($self, $engine) = @_; return @{ $self->get_save_cache($engine)->{INSERTS} }; } sub get_insert_fields { my ($self, $engine) = @_; return @{ $self->get_save_cache($engine)->{INSERT_FIELDS} }; } sub get_update_statements { my ($self, $engine) = @_; return @{ $self->get_save_cache($engine)->{UPDATES} }; } sub get_update_fields { my ($self, $engine) = @_; return @{ $self->get_save_cache($engine)->{UPDATE_FIELDS} }; } sub get_save_cache { my ($class, $engine) = @_; return $class->{SAVE} ||= do { my $schema = $engine->{SCHEMA}; my $id_col = $schema->{sql}{id_col}; my $type_col = $engine->{TYPE_COL}; my (%tables, @tables); my (@export_sources, @export_closures); my $context = { layout1 => $engine->{layout1} }; my $field_index = 2; $class->for_composing (sub { my ($part) = @_; my $table_name = $part->{MAPPING}{table}; my $table = $tables{$table_name} ||= do { push @tables, my $table = [ $table_name, [], [] ]; $table }; $context->{class} = $part; for my $field ($part->{MAPPING}->get_direct_fields()) { my @export_cols = $field->get_export_cols($context); push @{ $table->[1] }, @export_cols; push @{ $table->[2] }, $field_index..($field_index + $#export_cols); $field_index += @export_cols; } }); my (@inserts, @updates, @insert_fields, @update_fields); for my $table (@tables) { my ($table_name, $cols, $fields) = @$table; my @meta = ( $id_col ); my @meta_fields = ( 0 ); if ($engine->{ROOT_TABLES}{$table_name}) { push @meta, $type_col; push @meta_fields, 1; } next unless @meta > 1 || @$cols; push @inserts, sprintf("INSERT INTO %s\n (%s)\nVALUES\n (%s)", $table_name, join(', ', @meta, @$cols), join(', ', ('?') x (@meta + @$cols))); push @insert_fields, [ @meta_fields, @$fields ]; if (@$cols) { push @updates, sprintf("UPDATE\n %s\nSET\n%s\nWHERE\n %s = ?", $table_name, join(",\n", map { " $_ = ?" } @$cols), $id_col); push @update_fields, [ @$fields, 0 ]; } } { INSERT_FIELDS => \@insert_fields, INSERTS => \@inserts, UPDATE_FIELDS => \@update_fields, UPDATES => \@updates, } }; } sub get_deletes { my ($self, $engine) = @_; return @{ $self->{DELETE} ||= do { my $schema = $engine->{SCHEMA}; my $context = { engine => $engine, schema => $schema, layout1 => $engine->{layout1} }; my (@tables, %seen); $self->for_composing (sub { my ($part) = @_; my $mapping = $part->{MAPPING}; my $home_table = $mapping->{table}; push @tables, $home_table if $mapping->is_root() && !$seen{$home_table}++; $context->{class} = $part->{CLASS}; for my $qcol ($mapping->get_export_cols($context)) { my ($table) = @$qcol; push @tables, $table unless $seen{$table}++; } }); my $id_col = $engine->{SCHEMA}{sql}{id_col}; [ map { "DELETE FROM $_ WHERE $id_col = ?" } @tables ] } }; } sub get_table_set { my ($self, $engine) = @_; # return the TableSet on which the object's state resides # It doesn't include tables resulting solely from an intrusion. # Tables that carry only meta-information are also included. return $self->{TABLE_SET} ||= do { my $mapping = $self->{MAPPING}; my $home_table = $mapping->{table}; my $context = { layout1 => $engine->{layout1}, class => $self->{CLASS} }; my @table = map { $_->[0] } $mapping->get_export_cols($context); push @table, $home_table if $engine->{ROOT_TABLES}{$home_table}; Tangram::Relational::TableSet ->new((map { $_->get_table_set($engine)->tables } $self->direct_bases()), @table ); }; } sub get_polymorphic_select { my ($self, $engine, $storage) = @_; my $selects = $self->{POLYMORPHIC_SELECT} ||= do { my $schema = $engine->{SCHEMA}; my $id_col = $schema->{sql}{id_col}; my $type_col = $engine->{TYPE_COL}; my $context = { engine => $engine, schema => $schema, layout1 => $engine->{layout1} }; my $table_set = $self->get_table_set($engine); my %base_tables = do { my $ph = 0; map { $_ => $ph++ } $table_set->tables() }; my %partition; $self->for_conforming (sub { my $conforming = shift; my $key = $conforming->get_table_set($engine)->key; push @{ $partition{ $key } }, $conforming unless $conforming->{CLASS}{abstract}; }); my @selects; for my $table_set_key (keys %partition) { my $mates = $partition{$table_set_key}; my $table_set = $mates->[0]->get_table_set($engine); my @tables = $table_set->tables(); my %slice; my %col_index; my $col_mark = 0; my (@cols, @expand); my $root_table = $tables[0]; push @cols, qualify($id_col, $root_table, \%base_tables, \@expand); push @cols, qualify($type_col, $root_table, \%base_tables, \@expand); my %used; $used{$root_table} += 2; for my $mate (@$mates) { my @slice; $mate->for_composing (sub { my ($composing) = @_; my $table = $composing->{MAPPING}{table}; $context->{class} = $composing; my @direct_fields = $composing->{MAPPING}->get_direct_fields(); for my $field (@direct_fields) { my @import_cols = $field->get_import_cols($context); $used{$table} += @import_cols; for my $col (@import_cols) { my $qualified_col = "$table.$col"; unless (exists $col_index{$qualified_col}) { push @cols, qualify($col, $table, \%base_tables, \@expand); $col_index{$qualified_col} = $col_mark++; } push @slice, $col_index{$qualified_col}; } } }); $slice{ $storage->{class2id}{$mate->{CLASS}{name}} || $mate->{MAPPING}{id} } = \@slice; # should be $mate->{id} (compat) } my @from; for my $table (@tables) { next unless $used{$table}; if (exists $base_tables{$table}) { push @expand, $base_tables{$table}; push @from, "$table t%d"; } else { push @from, $table; } } my @where = (map { (qualify($id_col, $root_table, \%base_tables, \@expand) . ' = ' . qualify($id_col, $_, \%base_tables, \@expand) ) } grep { $used{$_} } @tables[1..$#tables] ); unless ( ($storage->{compat} and $storage->{compat} le "2.08") or @$mates == $engine->get_heterogeneity($table_set)) { my @type_ids = (map { # try $storage first for compatibility # with layout1 $storage->{class2id}{$_->{CLASS}{name}} or $_->{MAPPING}{id} } @$mates); my $column = qualify($type_col, $root_table, \%base_tables, \@expand); if ( @type_ids == 1 ) { push @where, "$column = @type_ids"; } else { push @where, "$column IN (". (join ', ', @type_ids). ")"; } } push @selects, Tangram::Relational::PolySelectTemplate ->new(\@expand, \@cols, \@from, \@where, \%slice); } \@selects; }; return @$selects; } sub qualify { my ($col, $table, $ph, $expand) = @_; if (exists $ph->{$table}) { push @$expand, $ph->{$table}; return "t%d.$col"; } else { return "$table.$col"; } } # XXX - never reached (?) sub get_exporter { my ($self, $context) = @_; return $self->{EXPORTER} ||= do { my (@export_sources, @export_closures); $self->for_composing (sub { my ($composing) = @_; my $class = $composing->{CLASS}; $context->{class} = $class; for my $field ($composing->{MAPPING}->get_direct_fields()) { if (my $exporter = $field->get_exporter($context)) { if (ref $exporter) { push @export_closures, $exporter; push @export_sources, 'shift(@closures)->($obj, $context)'; } else { push @export_sources, $exporter; } } } }); my $export_source = join ",\n", @export_sources; my $copy_closures = ( @export_closures ? ' my @closures = @export_closures;' : '' ); $export_source = ("sub { my (\$obj, \$context) = \@_;" ."$copy_closures\n$export_source }"); print $Tangram::TRACE ("Compiling exporter for $self->{name}..." ."\n$export_source\n") if $Tangram::TRACE; eval $export_source or die; } } # XXX - never reached (?) sub get_importer { my ($self, $context) = @_; return $self->{IMPORTER} ||= do { my (@import_sources, @import_closures); $self->for_composing ( sub { my ($composing) = @_; my $class = $composing->{CLASS}; $context->{class} = $class; for my $field ($composing->{MAPPING}->get_direct_fields()) { my $importer = $field->get_importer($context) or next; if (ref $importer) { push @import_closures, $importer; push @import_sources, 'shift(@closures)->($obj, $row, $context)'; } else { push @import_sources, $importer; } } } ); my $import_source = join ";\n", @import_sources; my $copy_closures = ( @import_closures ? ' my @closures = @import_closures;' : '' ); # $Tangram::TRACE = \*STDOUT; $import_source = ("sub { my (\$obj, \$row, \$context) = \@_;" ."$copy_closures\n$import_source }"); print $Tangram::TRACE ("Compiling importer for $self->{name}:" ."\n$import_source\n") if $Tangram::TRACE; # use Data::Dumper; print Dumper \@cols; eval $import_source or die; }; } 1;