Created
September 4, 2009 15:27
-
-
Save metaperl/180942 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #---------------------------------------------------------------------- | |
| package DBIx::DataModel::Schema::Generator; | |
| #---------------------------------------------------------------------- | |
| # see POD doc at end of file | |
| # version : see DBIx::DataModel | |
| use strict; | |
| use warnings; | |
| no warnings 'uninitialized'; | |
| use Carp; | |
| use List::Util qw/max/; | |
| use Exporter qw/import/; | |
| use DBI; | |
| our @EXPORT = qw/fromDBIxClass fromDBI/; | |
| sub new { | |
| my ($class, @args) = @_; | |
| my $self = bless {@args}, $class; | |
| $self->{-schema} ||= "My::Schema"; | |
| return $self; | |
| } | |
| sub fromDBI { | |
| # may be called as ordinary sub or as method | |
| my $self = ref $_[0] eq __PACKAGE__ ? shift : __PACKAGE__->new(@ARGV); | |
| my $dsn = shift or croak "missing arg (dsn for DBI->connect(..))"; | |
| my $user = shift || ""; | |
| my $passwd = shift || ""; | |
| my $options = shift || {RaiseError => 1}; | |
| my $dbh = DBI->connect($dsn, $user, $passwd, $options) | |
| or croak "DBI->connect failed ($DBI::errstr)"; | |
| my %args | |
| = (catalog => undef, schema => undef, table => undef, type => "TABLE"); | |
| my $tables_sth = $dbh->table_info(@args{qw/catalog schema table type/}); | |
| my $tables = $tables_sth->fetchall_arrayref({}); | |
| TABLE: | |
| foreach my $table (@$tables) { | |
| # get primary key info | |
| my @table_id = @{$table}{qw/TABLE_CAT TABLE_SCHEM TABLE_NAME/}; | |
| my $pkey = join(" ", $dbh->primary_key(@table_id)) || "unknown_pk"; | |
| my $table_info = { | |
| classname => _table2class($table->{TABLE_NAME}), | |
| tablename => $table->{TABLE_NAME}, | |
| pkey => $pkey, | |
| remarks => $table->{REMARKS}, | |
| }; | |
| # insert into list of tables | |
| push @{$self->{tables}}, $table_info; | |
| # get association info (in an eval because unimplemented by some drivers) | |
| my $fkey_sth = eval {$dbh->foreign_key_info(@table_id, | |
| undef, undef, undef)} | |
| or next TABLE; | |
| while (my $fk_row = $fkey_sth->fetchrow_hashref) { | |
| # hack for unifying "ODBC" or "SQL/CLI" column names (see L<DBI>) | |
| $fk_row->{"UK_$_"} ||= $fk_row->{"PK$_"} for qw/TABLE_NAME COLUMN_NAME/; | |
| $fk_row->{"FK_$_"} ||= $fk_row->{"FK$_"} for qw/TABLE_NAME COLUMN_NAME/; | |
| my @assoc = ( | |
| { table => _table2class($fk_row->{UK_TABLE_NAME}), | |
| col => $fk_row->{UK_COLUMN_NAME}, | |
| role => _table2role($fk_row->{UK_TABLE_NAME}), | |
| mult_min => 1, #0/1 (TODO: depend on is_nullable on other side) | |
| mult_max => 1, | |
| }, | |
| { table => _table2class($fk_row->{FK_TABLE_NAME}), | |
| col => $fk_row->{FK_COLUMN_NAME}, | |
| role => _table2role($fk_row->{FK_TABLE_NAME}, "s"), | |
| mult_min => 0, | |
| mult_max => '*', | |
| } | |
| ); | |
| push @{$self->{assoc}}, \@assoc; | |
| } | |
| } | |
| $self->generate; | |
| } | |
| sub fromDBIxClass { | |
| # may be called as ordinary sub or as method | |
| my $self = ref $_[0] eq __PACKAGE__ ? shift : __PACKAGE__->new(@ARGV); | |
| my $dbic_schema = shift or croak "missing arg (DBIC schema name)"; | |
| # load the DBIx::Class schema | |
| eval "use $dbic_schema; 1;" or croak $@; | |
| # global hash to hold assoc. info (because we must collect info from | |
| # both tables to get both directions of the association) | |
| my %associations; | |
| # foreach DBIC table class ("moniker" : short class name) | |
| foreach my $moniker ($dbic_schema->sources) { | |
| my $source = $dbic_schema->source($moniker); # full DBIC class | |
| # table info | |
| my $table_info = { | |
| classname => $moniker, | |
| tablename => $source->from, | |
| pkey => join(" ", $source->primary_columns), | |
| }; | |
| # inflated columns | |
| foreach my $col ($source->columns) { | |
| my $column_info = $source->column_info($col); | |
| my $inflate_info = $column_info->{_inflate_info} | |
| or next; | |
| # don't care about inflators for related objects | |
| next if $source->relationship_info($col); | |
| my $data_type = $column_info->{data_type}; | |
| push @{$self->{column_types}{$data_type}{$moniker}}, $col; | |
| } | |
| # insert into list of tables | |
| push @{$self->{tables}}, $table_info; | |
| # association info | |
| foreach my $relname ($source->relationships) { | |
| my $relinfo = $source->relationship_info($relname); | |
| # extract join keys from $relinfo->{cond} (which | |
| # is of shape {"foreign.k1" => "self.k2"}) | |
| my ($fk, $pk) = map /\.(.*)/, %{$relinfo->{cond}}; | |
| # moniker of the other side of the relationship | |
| my $relmoniker = $source->related_source($relname)->source_name; | |
| # info structure | |
| my %info = ( | |
| table => $relmoniker, | |
| col => $fk, | |
| role => $relname, | |
| # compute multiplicities | |
| mult_min => $relinfo->{attrs}{join_type} eq 'LEFT' ? 0 : 1, | |
| mult_max => $relinfo->{attrs}{accessor} eq 'multi' ? "*" : 1, | |
| ); | |
| # store assoc info into global hash; since both sides of the assoc must | |
| # ultimately be joined, we compute a unique key from alphabetic ordering | |
| my ($key, $index) = ($moniker cmp $relmoniker || $fk cmp $pk) < 0 | |
| ? ("$moniker/$relmoniker/$fk/$pk", 0) | |
| : ("$relmoniker/$moniker/$pk/$fk", 1); | |
| $associations{$key}[$index] = \%info; | |
| # info on other side of the association | |
| my $other_index = 1 - $index; | |
| my $other_assoc = $associations{$key}[1 - $index] ||= {}; | |
| $other_assoc->{table} ||= $moniker; | |
| $other_assoc->{col} ||= $pk; | |
| defined $other_assoc->{mult_min} or $other_assoc->{mult_min} = 1; | |
| defined $other_assoc->{mult_max} or $other_assoc->{mult_max} = 1; | |
| } | |
| } | |
| $self->{assoc} = [values %associations]; | |
| $self->generate; | |
| } | |
| # other name for this method | |
| *fromDBIC = \&fromDBIxClass; | |
| sub generate { | |
| my ($self) = @_; | |
| my $pkg = $self->{-schema}; | |
| use IO qw(File Handle); | |
| my $io = do | |
| { | |
| if (my $dir = $self->{-dir}) | |
| { | |
| my $file = sprintf '%s/%s.pm', $dir, $pkg; | |
| my $tmp = IO::File->new($file, 'w'); | |
| $tmp; | |
| } | |
| else | |
| { | |
| my $tmp = IO::Handle->new; | |
| die "open failed: $!" unless $tmp->fdopen(fileno(STDOUT),"w"); | |
| $tmp; | |
| } | |
| }; | |
| # compute max length of various fields (for prettier source alignment) | |
| my %l; | |
| foreach my $field (qw/classname tablename pkey/) { | |
| $l{$field} = max map {length $_->{$field}} @{$self->{tables}}; | |
| } | |
| foreach my $field (qw/col role mult/) { | |
| $l{$field} = max map {length $_->{$field}} map {(@$_)} @{$self->{assoc}}; | |
| } | |
| $l{mult} = max ($l{mult}, 4); | |
| # start emitting code | |
| $io->print(<<__END_OF_CODE__); | |
| package $pkg; | |
| use strict; | |
| use warnings; | |
| use DBIx::DataModel; | |
| DBIx::DataModel # no semicolon (intentional) | |
| #---------------------------------------------------------------------# | |
| # SCHEMA DECLARATION # | |
| #---------------------------------------------------------------------# | |
| ->Schema('$pkg') | |
| #---------------------------------------------------------------------# | |
| # TABLE DECLARATIONS # | |
| #---------------------------------------------------------------------# | |
| __END_OF_CODE__ | |
| my $colsizes = "%-$l{classname}s %-$l{tablename}s %-$l{pkey}s"; | |
| my $format = "->Table(qw/$colsizes/)\n"; | |
| $io->printf( "# $colsizes\n", qw/Class Table PK/); | |
| $io->printf( "# $colsizes\n", qw/===== ===== ==/); | |
| foreach my $table (@{$self->{tables}}) { | |
| if ($table->{remarks}) { | |
| $table->{remarks} =~ s/^/# /gm; | |
| $io->print("\n$table->{remarks}\n"); | |
| } | |
| $io->printf($format, $table->{classname}, $table->{tablename}, $table->{pkey}); | |
| } | |
| $colsizes = "%-$l{classname}s %-$l{role}s %-$l{mult}s %-$l{col}s"; | |
| $format = " [qw/$colsizes/]"; | |
| $io->print(<<__END_OF_CODE__); | |
| #---------------------------------------------------------------------# | |
| # ASSOCIATION DECLARATIONS # | |
| #---------------------------------------------------------------------# | |
| __END_OF_CODE__ | |
| $io->printf( "# $colsizes\n", qw/Class Role Mult Join/); | |
| $io->printf( "# $colsizes", qw/===== ==== ==== ====/); | |
| foreach my $a (@{$self->{assoc}}) { | |
| # for prettier output, make sure that multiplicity "1" is first | |
| @$a = reverse @$a if $a->[1]{mult_max} eq "1" | |
| && $a->[0]{mult_max} eq "*"; | |
| # complete association info | |
| for my $i (0, 1) { | |
| $a->[$i]{role} ||= "---"; | |
| my $mult = "$a->[$i]{mult_min}..$a->[$i]{mult_max}"; | |
| $a->[$i]{mult} = {"0..*" => "*", "1..1" => "1"}->{$mult} || $mult; | |
| } | |
| $io->print( "\n->Association(\n"); | |
| $io->printf( $format, @{$a->[0]}{qw/table role mult col/}); | |
| $io->print( ",\n"); | |
| $io->printf( $format, @{$a->[1]}{qw/table role mult col/}); | |
| $io->print( ")\n"); | |
| } | |
| $io->print( "\n;\n"); | |
| # column types | |
| $io->print(<<__END_OF_CODE__); | |
| #---------------------------------------------------------------------# | |
| # COLUMN TYPES # | |
| #---------------------------------------------------------------------# | |
| # $self->{-schema}->ColumnType(ColType_Example => | |
| # fromDB => sub {...}, | |
| # toDB => sub {...}); | |
| # $self->{-schema}::SomeTable->ColumnType(ColType_Example => | |
| # qw/column1 column2 .../); | |
| __END_OF_CODE__ | |
| while (my ($type, $targets) = each %{$self->{column_types} || {}}) { | |
| $io->print(<<__END_OF_CODE__); | |
| # $type | |
| $self->{-schema}->ColumnType($type => | |
| fromDB => sub {}, # SKELETON .. PLEASE FILL IN | |
| toDB => sub {}); | |
| __END_OF_CODE__ | |
| while (my ($table, $cols) = each %$targets) { | |
| $io->printf | |
| ( | |
| "%s::%s->ColumnType($type => qw/%s/);\n", | |
| $self->{-schema}, $table, join(" ", @$cols) | |
| ); | |
| } | |
| $io->print("\n"); | |
| } | |
| # end of module | |
| $io->print("\n\n1;\n"); | |
| } | |
| # support for SQL::Translator::Producer | |
| sub produce { | |
| my $tr = shift; | |
| my $self = __PACKAGE__->new(%{$tr->{producer_args} || {}}); | |
| my $schema = $tr->schema; | |
| foreach my $table ($schema->get_tables) { | |
| my $tablename = $table->name; | |
| my $classname = _table2class($tablename); | |
| my $pk = $table->primary_key; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment