package SQL::Translator;
# ----------------------------------------------------------------------
-# $Id: Translator.pm,v 1.61 2004-11-09 05:27:45 grommit Exp $
+# $Id: Translator.pm,v 1.62 2004-12-12 18:38:11 grommit Exp $
# ----------------------------------------------------------------------
# Copyright (C) 2002-4 The SQLFairy Authors
#
require 5.004;
$VERSION = '0.06';
-$REVISION = sprintf "%d.%02d", q$Revision: 1.61 $ =~ /(\d+)\.(\d+)/;
+$REVISION = sprintf "%d.%02d", q$Revision: 1.62 $ =~ /(\d+)\.(\d+)/;
$DEBUG = 0 unless defined $DEBUG;
$ERROR = "";
}
#
+ # Initialize the filters.
+ #
+ if ( $config->{filters} && ref $config->{filters} eq "ARRAY" ) {
+ $self->filters( @{$config->{filters}} )
+ || return $self->error('Error inititializing filters: '.$self->error);
+ }
+
+ #
# Set the data source, if 'filename' or 'file' is provided.
#
$config->{'filename'} ||= $config->{'file'} || "";
$self->filename( $config->{'filename'} ) if $config->{'filename'};
#
- # Finally, if there is a 'data' parameter, use that in
+ # Finally, if there is a 'data' parameter, use that in
# preference to filename and file
#
if ( my $data = $config->{'data'} ) {
$self->{'debug'} = defined $config->{'debug'} ? $config->{'debug'} : $DEBUG;
$self->add_drop_table( $config->{'add_drop_table'} );
-
+
$self->no_comments( $config->{'no_comments'} );
$self->show_warnings( $config->{'show_warnings'} );
# ----------------------------------------------------------------------
sub producer {
shift->_tool({
- name => 'producer',
+ name => 'producer',
path => "SQL::Translator::Producer",
- default_sub => "produce"
+ default_sub => "produce",
}, @_);
}
# ----------------------------------------------------------------------
sub parser {
shift->_tool({
- name => 'parser',
+ name => 'parser',
path => "SQL::Translator::Parser",
- default_sub => "parse"
+ default_sub => "parse",
}, @_);
}
sub parser_args { shift->_args("parser", @_); }
# ----------------------------------------------------------------------
+# e.g.
+# $sqlt->filters => [
+# sub { },
+# [ "NormalizeNames", field => "lc", tabel => "ucfirst" ],
+# [
+# "DataTypeMap",
+# "TEXT" => "BIGTEXT",
+# ],
+# ],
+# ----------------------------------------------------------------------
+sub filters {
+ my $self = shift;
+ my $filters = $self->{filters} ||= [];
+ return @$filters unless @_;
+
+ # Set. Convert args to list of [\&code,\%args]
+ foreach (@_) {
+ $_ = [$_,{}] if not ref($_) eq "ARRAY";
+ my ($name,$args) = @$_;
+ if ( isa($name,"CODE") ) {
+ push @$filters, $_;
+ next;
+ }
+ else {
+ $self->debug("Adding $name filter. Args:".Dumper($args)."\n");
+ my $code = _load_sub("$name\::filter", "SQL::Translator::Filter");
+ return $self->error("ERROR:".$self->error) unless $code;
+ push @$filters, [$code,$args];
+ }
+ }
+ return @$filters;
+}
+
+# ----------------------------------------------------------------------
sub show_warnings {
my $self = shift;
my $arg = shift;
my ($parser_output, $producer_output);
# Parse arguments
- if (@_ == 1) {
+ if (@_ == 1) {
# Passed a reference to a hash?
if (isa($_[0], 'HASH')) {
# yep, a hashref
$producer_type = $self->producer_type;
# ----------------------------------------------------------------
- # Execute the parser, then execute the producer with that output.
+ # Execute the parser, the filters and then execute the producer.
# Allowances are made for each piece to die, or fail to compile,
# since the referenced subroutines could be almost anything. In
# the future, each of these might happen in a Safe environment,
# depending on how paranoid we want to be.
# ----------------------------------------------------------------
+
+ # Run parser
unless ( defined $self->{'schema'} ) {
eval { $parser_output = $parser->($self, $$data) };
if ($@ || ! $parser_output) {
return $self->error($msg);
}
}
-
$self->debug("Schema =\n", Dumper($self->schema), "\n");
+ # Validate the schema if asked to.
if ($self->validate) {
my $schema = $self->schema;
return $self->error('Invalid schema') unless $schema->is_valid;
}
+ # Run filters
+ my $filt_num = 0;
+ foreach ($self->filters) {
+ $filt_num++;
+ my ($code,$args) = @$_;
+ eval { $code->($self->schema, $args) };
+ my $err = $@ || $self->error || 0;
+ return $self->error("Error with filter $filt_num : $err") if $err;
+ }
+
+ # Run producer
eval { $producer_output = $producer->($self) };
if ($@ || ! $producer_output) {
my $err = $@ || $self->error || "no results";
my ($code,$sub);
($code,$sub) = _load_sub("$tool\::$default_sub", $path);
($code,$sub) = _load_sub("$tool", $path) unless $code;
-
+
# get code reference and assign
my (undef,$module,undef) = $sub =~ m/((.*)::)?(\w+)$/;
$self->{$name} = $code;
#
# Returns package name of the module actually loaded or false and sets error.
#
-# Note, you can't load a name from the root namespace (ie one without '::' in
+# Note, you can't load a name from the root namespace (ie one without '::' in
# it), therefore a single word name without a path fails.
# ----------------------------------------------------------------------
sub load {
my $file = $module; $file =~ s[::][/]g; $file .= ".pm";
__PACKAGE__->debug("Loading $name as $file\n");
return $module if $INC{$file}; # Already loaded
-
+
eval { require $file };
next if $@ =~ /Can't locate $file in \@INC/;
eval { $file->import(@_) } unless $@;
# ----------------------------------------------------------------------
sub _load_sub {
my ($tool, @path) = @_;
-
- # Passed a module name or module and sub name
+
+ # Passed a module name or module and sub name
my (undef,$module,$func_name) = $tool =~ m/((.*)::)?(\w+)$/;
if ( my $module = load($module => @path) ) {
my $sub = "$module\::$func_name";
- return ( \&{ $sub }, $sub );
- }
+ return wantarray ? ( \&{ $sub }, $sub ) : \&$sub;
+ }
return undef;
}
}
# ----------------------------------------------------------------------
-# The other format_*_name methods rely on this one. It optionally
+# The other format_*_name methods rely on this one. It optionally
# accepts a subroutine ref as the first argument (or uses an identity
# sub if one isn't provided or it doesn't already exist), and applies
# it to the rest of the arguments (if any).
# Print debug info
debug => 1,
# Print Parse::RecDescent trace
- trace => 0,
+ trace => 0,
# Don't include comments in output
- no_comments => 0,
+ no_comments => 0,
# Print name mutations, conflicts
- show_warnings => 0,
+ show_warnings => 0,
# Add "drop table" statements
- add_drop_table => 1,
+ add_drop_table => 1,
# Validate schema object
- validate => 1,
+ validate => 1,
# Make all table names CAPS in producers which support this option
format_table_name => sub {my $tablename = shift; return uc($tablename)},
# Null-op formatting, only here for documentation's sake
from => 'MySQL',
to => 'Oracle',
# Or an arrayref of filenames, i.e. [ $file1, $file2, $file3 ]
- filename => $file,
+ filename => $file,
) or die $translator->error;
print $output;
=item *
+filters
+
+=item *
+
filename / file
=item *
There is also C<parser_type> and C<parser_args>, which perform
analogously to C<producer_type> and C<producer_args>
+=head2 filters
+
+Set or retreive the filters to run over the schema during the
+translation, before the producer creates its output. Filters are sub
+routines called, in order, with the schema object to filter as the 1st
+arg and a hashref of options as the 2nd. They are free to do whatever
+they want to the schema object, which will be handed to any following
+filters, then used by the producer.
+
+Filters are set as an array, which gives the order they run in.
+Like parsers and producers, they can be defined by a module name, a
+module name relative to the SQL::Translator::Filter namespace, a module
+name and function name together or a reference to an anonymous subroutine.
+When using a module name a function called C<filter> will be invoked in
+that package to do the work. To pass args to the filter set it as an array
+ref with the 1st value giving the filter and the rest being a hash of
+args.
+
+ $tr->filters(
+ sub {
+ my $schema = shift;
+ # Do stuff to schema here!
+ },
+ [ "Foo", foo => "bar", hello => "world" ],
+ [ "Filter3" ],
+ );
+
+Although you would normally set them in the constructor, which calls
+through to filters. i.e.
+
+ my $translator = SQL::Translator->new(
+ ...
+ filters => [
+ sub { ... },
+ [ Foo, foo => "bar" ],
+ ],
+ ...
+ );
+
+See F<t/36-filters.t> for more examples.
+
+Multiple set calls to filters are cumulative with new filters added to
+the end of the current list.
+
+Returns the filters as a list of array refs, the 1st value being a
+reference to the filter sub routine and the 2nd a hashref its args.
+
=head2 show_warnings
Toggles whether to print warnings of name conflicts, identifier
=head2 translate
-The C<translate> method calls the subroutines referenced by the
-C<parser> and C<producer> data members (described above). It accepts
-as arguments a number of things, in key => value format, including
-(potentially) a parser and a producer (they are passed directly to the
-C<parser> and C<producer> methods).
+The C<translate> method calls the subroutine referenced by the
+C<parser> data member, then calls any C<filters> and finally calls
+the C<producer> sub routine (these members are described above).
+It accepts as arguments a number of things, in key => value format,
+including (potentially) a parser and a producer (they are passed
+directly to the C<parser> and C<producer> methods).
Here is how the parameter list to C<translate> is parsed:
--- /dev/null
+#!/usr/bin/perl -w
+# vim:filetype=perl
+
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+# SQL::Translator::Filter::HelloWorld - Test filter in a package
+#=============================================================================
+package SQL::Translator::Filter::HelloWorld;
+
+use strict;
+use vars qw/$VERSION/;
+$VERSION=0.1;
+
+sub filter {
+ my ($schema,$args) = (shift,shift);
+
+ my $greeting = $args->{greeting} || "Hello";
+ $schema->add_table(
+ name => "HelloWorld",
+ );
+}
+
+# Hack to allow sqlt to see our module as it wasn't loaded from a .pm
+$INC{'SQL/Translator/Filter/HelloWorld.pm'}
+ = 'lib/SQL/Translator/Filter/HelloWorld.pm';
+
+#=============================================================================
+
+package main;
+
+use strict;
+use Test::More;
+use Test::Exception;
+use Test::SQL::Translator qw(maybe_plan);
+
+use Data::Dumper;
+
+BEGIN {
+ maybe_plan(14, 'Template', 'Test::Differences')
+}
+use Test::Differences;
+use SQL::Translator;
+
+my $in_yaml = qq{--- #YAML:1.0
+schema:
+ tables:
+ person:
+ name: person
+ fields:
+ first_name:
+ data_type: foovar
+ name: First_Name
+};
+
+my $ans_yaml = qq{--- #YAML:1.0
+schema:
+ procedures: {}
+ tables:
+ HelloWorld:
+ comments: ''
+ constraints: []
+ fields: {}
+ indices: []
+ name: HelloWorld
+ options: []
+ order: 2
+ PERSON:
+ comments: ''
+ constraints: []
+ fields:
+ first_name:
+ data_type: foovar
+ default_value: ~
+ extra: {}
+ is_nullable: 1
+ is_primary_key: 0
+ is_unique: 0
+ name: first_name
+ order: 1
+ size:
+ - 0
+ indices: []
+ name: PERSON
+ options: []
+ order: 1
+ triggers: {}
+ views: {}
+translator:
+ add_drop_table: 0
+ filename: ~
+ no_comments: 0
+ parser_args: {}
+ parser_type: SQL::Translator::Parser::YAML
+ producer_args: {}
+ producer_type: SQL::Translator::Producer::YAML
+ show_warnings: 1
+ trace: 0
+ version: 0.06
+};
+
+# Parse the test XML schema
+my $obj;
+$obj = SQL::Translator->new(
+ debug => 0,
+ show_warnings => 1,
+ parser => "YAML",
+ data => $in_yaml,
+ to => "YAML",
+ filters => [
+ # Check they get called ok
+ sub {
+ pass("Filter 1 called");
+ isa_ok($_[0],"SQL::Translator::Schema", "Filter 1, arg0 ");
+ ok( ref($_[1]) eq "HASH", "Filter 1, arg1 is a hashref ");
+ },
+ sub {
+ pass("Filter 2 called");
+ isa_ok($_[0],"SQL::Translator::Schema", "Filter 2, arg0 ");
+ ok( ref($_[1]) eq "HASH", "Filter 2, arg1 is a hashref ");
+ },
+
+ # Sub filter with args
+ [ sub {
+ pass("Filter 3 called");
+ isa_ok($_[0],"SQL::Translator::Schema", "Filter 3, arg0 ");
+ ok( ref($_[1]) eq "HASH", "Filter 3, arg1 is a hashref ");
+ is( $_[1]->{hello}, "world", "Filter 3, got args ");
+ },
+ { hello=>"world" } ],
+
+ # Uppercase all the table names.
+ sub {
+ my $schema = shift;
+ foreach ($schema->get_tables) {
+ $_->name(uc $_->name);
+ }
+ },
+
+ # lowercase all the field names.
+ sub {
+ my $schema = shift;
+ foreach ( map { $_->get_fields } $schema->get_tables ) {
+ $_->name(lc $_->name);
+ }
+ },
+
+ # Filter from SQL::Translator::Filter::*
+ [ 'HelloWorld' ],
+ ],
+
+) or die "Failed to create translator object: ".SQL::Translator->error;
+
+my $out;
+lives_ok { $out = $obj->translate; } "Translate ran";
+is $obj->error, '' ,"No errors";
+ok $out ne "" ,"Produced something!";
+eq_or_diff $out, $ans_yaml ,"Output looks right";