Made some changes suggested by Michael Slattery to fix table level comments. Also...
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / XML / SQLFairy.pm
1 package SQL::Translator::Producer::XML::SQLFairy;
2
3 # -------------------------------------------------------------------
4 # $Id: SQLFairy.pm,v 1.20 2005-07-05 16:20:43 mwz444 Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2003 Ken Y. Clark <kclark@cpan.org>,
7 #                    darren chamberlain <darren@cpan.org>,
8 #                    Chris Mungall <cjm@fruitfly.org>,
9 #                    Mark Addison <mark.addison@itn.co.uk>.
10 #
11 # This program is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU General Public License as
13 # published by the Free Software Foundation; version 2.
14 #
15 # This program is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 # 02111-1307  USA
24 # -------------------------------------------------------------------
25
26 =pod
27
28 =head1 NAME
29
30 SQL::Translator::Producer::XML::SQLFairy - SQLFairy's default XML format
31
32 =head1 SYNOPSIS
33
34   use SQL::Translator;
35
36   my $t              = SQL::Translator->new(
37       from           => 'MySQL',
38       to             => 'XML-SQLFairy',
39       filename       => 'schema.sql',
40       show_warnings  => 1,
41   );
42
43   print $t->translate;
44
45 =head1 DESCRIPTION
46
47 Creates XML output of a schema, in the flavor of XML used natively by the
48 SQLFairy project (L<SQL::Translator>). This format is detailed here.
49
50 The XML lives in the C<http://sqlfairy.sourceforge.net/sqlfairy.xml> namespace.
51 With a root element of <schema>.
52
53 Objects in the schema are mapped to tags of the same name as the objects class
54 (all lowercase).
55
56 The attributes of the objects (e.g. $field->name) are mapped to attributes of
57 the tag, except for sql, comments and action, which get mapped to child data
58 elements.
59
60 List valued attributes (such as the list of fields in an index)
61 get mapped to comma seperated lists of values in the attribute.
62
63 Child objects, such as a tables fields, get mapped to child tags wrapped in a
64 set of container tags using the plural of their contained classes name.
65
66 An objects's extra attribute (a hash of arbitary data) is
67 mapped to a tag called extra, with the hash of data as attributes, sorted into
68 alphabetical order.
69
70 e.g.
71
72     <schema name="" database=""
73       xmlns="http://sqlfairy.sourceforge.net/sqlfairy.xml">
74
75       <tables>
76         <table name="Story" order="1">
77           <fields>
78             <field name="id" data_type="BIGINT" size="20"
79               is_nullable="0" is_auto_increment="1" is_primary_key="1"
80               is_foreign_key="0" order="3">
81               <extra ZEROFILL="1" />
82               <comments></comments>
83             </field>
84             <field name="created" data_type="datetime" size="0"
85               is_nullable="1" is_auto_increment="0" is_primary_key="0"
86               is_foreign_key="0" order="1">
87               <extra />
88               <comments></comments>
89             </field>
90             ...
91           </fields>
92           <indices>
93             <index name="foobar" type="NORMAL" fields="foo,bar" options="" />
94           </indices>
95         </table>
96       </tables>
97
98       <views>
99         <view name="email_list" fields="email" order="1">
100           <sql>SELECT email FROM Basic WHERE email IS NOT NULL</sql>
101         </view>
102       </views>
103
104     </schema>
105
106 To see a complete example of the XML translate one of your schema :)
107
108   $ sqlt -f MySQL -t XML-SQLFairy schema.sql
109
110 =head1 ARGS
111
112 =over 4
113
114 =item add_prefix
115
116 Set to true to use the default namespace prefix of 'sqlf', instead of using
117 the default namespace for
118 C<http://sqlfairy.sourceforge.net/sqlfairy.xml namespace>
119
120 e.g.
121
122  <!-- add_prefix=0 -->
123  <field name="foo" />
124
125  <!-- add_prefix=1 -->
126  <sqlf:field name="foo" />
127
128 =item prefix
129
130 Set to the namespace prefix you want to use for the
131 C<http://sqlfairy.sourceforge.net/sqlfairy.xml namespace>
132
133 e.g.
134
135  <!-- prefix='foo' -->
136  <foo:field name="foo" />
137
138 =item newlines
139
140 If true (the default) inserts newlines around the XML, otherwise the schema is
141 written on one line.
142
143 =item indent
144
145 When using newlines the number of whitespace characters to use as the indent.
146 Default is 2, set to 0 to turn off indenting.
147
148 =back
149
150 =head1 LEGACY FORMAT
151
152 The previous version of the SQLFairy XML allowed the attributes of the the
153 schema objects to be written as either xml attributes or as data elements, in
154 any combination. The old producer could produce attribute only or data element
155 only versions. While this allowed for lots of flexibility in writing the XML
156 the result is a great many possible XML formats, not so good for DTD writing,
157 XPathing etc! So we have moved to a fixed version described above.
158
159 This version of the producer will now only produce the new style XML.
160 To convert your old format files simply pass them through the translator :)
161
162  $ sqlt -f XML-SQLFairy -t XML-SQLFairy schema-old.xml > schema-new.xml
163
164 =cut
165
166 use strict;
167 use vars qw[ $VERSION @EXPORT_OK ];
168 $VERSION = sprintf "%d.%02d", q$Revision: 1.20 $ =~ /(\d+)\.(\d+)/;
169
170 use Exporter;
171 use base qw(Exporter);
172 @EXPORT_OK = qw(produce);
173
174 use IO::Scalar;
175 use SQL::Translator::Utils qw(header_comment debug);
176 BEGIN {
177     # Will someone fix XML::Writer already?
178     local $^W = 0;
179     require XML::Writer;
180     import XML::Writer;
181 }
182
183 # Which schema object attributes (methods) to write as xml elements rather than
184 # as attributes. e.g. <comments>blah, blah...</comments>
185 my @MAP_AS_ELEMENTS = qw/sql comments action extra/;
186
187
188
189 my $Namespace = 'http://sqlfairy.sourceforge.net/sqlfairy.xml';
190 my $Name      = 'sqlf';
191 my $PArgs     = {};
192
193 sub produce {
194     my $translator  = shift;
195     my $schema      = $translator->schema;
196     $PArgs          = $translator->producer_args;
197     my $newlines    = defined $PArgs->{newlines} ? $PArgs->{newlines} : 1;
198     my $indent      = defined $PArgs->{indent}   ? $PArgs->{indent}   : 2;
199     my $io          = IO::Scalar->new;
200
201     # Setup the XML::Writer and set the namespace
202     my $prefix = "";
203     $prefix    = $Name            if $PArgs->{add_prefix};
204     $prefix    = $PArgs->{prefix} if $PArgs->{prefix};
205     my $xml         = XML::Writer->new(
206         OUTPUT      => $io,
207         NAMESPACES  => 1,
208         PREFIX_MAP  => { $Namespace => $prefix },
209         DATA_MODE   => $newlines,
210         DATA_INDENT => $indent,
211     );
212
213     # Start the document
214     $xml->xmlDecl('UTF-8');
215     $xml->comment(header_comment('', ''));
216     xml_obj($xml, $schema,
217         tag => "schema", methods => [qw/name database extra/], end_tag => 0 );
218
219     #
220     # Table
221     #
222     $xml->startTag( [ $Namespace => "tables" ] );
223     for my $table ( $schema->get_tables ) {
224         debug "Table:",$table->name;
225         xml_obj($xml, $table,
226              tag => "table",
227              methods => [qw/name order extra/],
228              end_tag => 0
229          );
230
231         #
232         # Fields
233         #
234         xml_obj_children( $xml, $table,
235             tag   => 'field',
236             methods =>[qw/
237                 name data_type size is_nullable default_value is_auto_increment
238                 is_primary_key is_foreign_key extra comments order
239             /],
240         );
241
242         #
243         # Indices
244         #
245         xml_obj_children( $xml, $table,
246             tag   => 'index',
247             collection_tag => "indices",
248             methods => [qw/name type fields options extra/],
249         );
250
251         #
252         # Constraints
253         #
254         xml_obj_children( $xml, $table,
255             tag   => 'constraint',
256             methods => [qw/
257                 name type fields reference_table reference_fields
258                 on_delete on_update match_type expression options deferrable
259                 extra
260             /],
261         );
262
263         #
264         # Comments
265         #
266         xml_obj_children( $xml, $table,
267             tag   => 'comment',
268             collection_tag => "comments",
269             methods => [qw/
270                 comments
271             /],
272         );
273
274         $xml->endTag( [ $Namespace => 'table' ] );
275     }
276     $xml->endTag( [ $Namespace => 'tables' ] );
277
278     #
279     # Views
280     #
281     xml_obj_children( $xml, $schema,
282         tag   => 'view',
283         methods => [qw/name sql fields order extra/],
284     );
285
286     #
287     # Tiggers
288     #
289     xml_obj_children( $xml, $schema,
290         tag    => 'trigger',
291         methods => [qw/name database_event action on_table perform_action_when
292             fields order extra/],
293     );
294
295     #
296     # Procedures
297     #
298     xml_obj_children( $xml, $schema,
299         tag   => 'procedure',
300         methods => [qw/name sql parameters owner comments order extra/],
301     );
302
303     $xml->endTag([ $Namespace => 'schema' ]);
304     $xml->end;
305
306     return $io;
307 }
308
309
310 #
311 # Takes and XML::Write object, Schema::* parent object, the tag name,
312 # the collection name and a list of methods (of the children) to write as XML.
313 # The collection name defaults to the name with an s on the end and is used to
314 # work out the method to get the children with. eg a name of 'foo' gives a
315 # collection of foos and gets the members using ->get_foos.
316 #
317 sub xml_obj_children {
318     my ($xml,$parent) = (shift,shift);
319     my %args = @_;
320     my ($name,$collection_name,$methods)
321         = @args{qw/tag collection_tag methods/};
322     $collection_name ||= "${name}s";
323
324     my $meth;
325     if ( $collection_name eq 'comments' ) {
326       $meth = 'comments';
327     } else {
328       $meth = "get_$collection_name";
329     }
330
331     my @kids = $parent->$meth;
332     #@kids || return;
333     $xml->startTag( [ $Namespace => $collection_name ] );
334
335     for my $obj ( @kids ) {
336         if ( $collection_name eq 'comments' ){
337             $xml->dataElement( [ $Namespace => 'comment' ], $obj );
338         } else {
339             xml_obj($xml, $obj,
340                 tag     => "$name",
341                 end_tag => 1,
342                 methods => $methods,
343             );
344         }
345     }
346     $xml->endTag( [ $Namespace => $collection_name ] );
347 }
348
349 #
350 # Takes an XML::Writer, Schema::* object and list of method names
351 # and writes the obect out as XML. All methods values are written as attributes
352 # except for the methods listed in @MAP_AS_ELEMENTS which get written as child
353 # data elements.
354 #
355 # The attributes/tags are written in the same order as the method names are
356 # passed.
357 #
358 # TODO
359 # - Should the Namespace be passed in instead of global? Pass in the same
360 #   as Writer ie [ NS => TAGNAME ]
361 #
362 my $elements_re = join("|", @MAP_AS_ELEMENTS);
363 $elements_re = qr/^($elements_re)$/;
364 sub xml_obj {
365     my ($xml, $obj, %args) = @_;
366     my $tag                = $args{'tag'}              || '';
367     my $end_tag            = $args{'end_tag'}          || '';
368     my @meths              = @{ $args{'methods'} };
369     my $empty_tag          = 0;
370
371     # Use array to ensure consistant (ie not hash) ordering of attribs
372     # The order comes from the meths list passed in.
373     my @tags;
374     my @attr;
375     foreach ( grep { defined $obj->$_ } @meths ) {
376         my $what = m/$elements_re/ ? \@tags : \@attr;
377         my $val = $_ eq 'extra'
378             ? { $obj->$_ }
379             : $obj->$_;
380         $val = ref $val eq 'ARRAY' ? join(',', @$val) : $val;
381         push @$what, $_ => $val;
382     };
383     my $child_tags = @tags;
384     $end_tag && !$child_tags
385         ? $xml->emptyTag( [ $Namespace => $tag ], @attr )
386         : $xml->startTag( [ $Namespace => $tag ], @attr );
387     while ( my ($name,$val) = splice @tags,0,2 ) {
388         if ( ref $val eq 'HASH' ) {
389              $xml->emptyTag( [ $Namespace => $name ],
390                  map { ($_, $val->{$_}) } sort keys %$val );
391         }
392         else {
393             $xml->dataElement( [ $Namespace => $name ], $val );
394         }
395     }
396     $xml->endTag( [ $Namespace => $tag ] ) if $child_tags && $end_tag;
397 }
398
399 1;
400
401 # -------------------------------------------------------------------
402 # The eyes of fire, the nostrils of air,
403 # The mouth of water, the beard of earth.
404 # William Blake
405 # -------------------------------------------------------------------
406
407 =pod
408
409 =head1 AUTHORS
410
411 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>,
412 Darren Chamberlain E<lt>darren@cpan.orgE<gt>,
413 Mark Addison E<lt>mark.addison@itn.co.ukE<gt>.
414
415 =head1 SEE ALSO
416
417 L<perl(1)>, L<SQL::Translator>, L<SQL::Translator::Parser::XML::SQLFairy>,
418 L<SQL::Translator::Schema>, L<XML::Writer>.
419
420 =cut