whitespace changes
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Parser / SQLServer.pm
1 package SQL::Translator::Parser::SQLServer;
2
3 # -------------------------------------------------------------------
4 # Copyright (C) 2002-2009 SQLFairy Authors
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; version 2.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
18 # 02111-1307  USA
19 # -------------------------------------------------------------------
20
21 =head1 NAME
22
23 SQL::Translator::Parser::SQLServer - parser for SQL Server
24
25 =head1 SYNOPSIS
26
27   use SQL::Translator::Parser::SQLServer;
28
29 =head1 DESCRIPTION
30
31 Adapted from Parser::Sybase and mostly parses the output of
32 Producer::SQLServer.  The parsing is by no means complete and
33 should probably be considered a work in progress.
34
35 =cut
36
37 use strict;
38
39 use vars qw[ $DEBUG $VERSION $GRAMMAR @EXPORT_OK ];
40 $VERSION = '1.59';
41 $DEBUG   = 0 unless defined $DEBUG;
42
43 use Data::Dumper;
44 use Parse::RecDescent;
45 use Exporter;
46 use base qw(Exporter);
47
48 @EXPORT_OK = qw(parse);
49
50 $::RD_ERRORS = 1;
51 $::RD_WARN   = 1;
52 $::RD_HINT   = 1;
53
54 $GRAMMAR = q{
55
56 {
57     my ( %tables, @table_comments, $table_order, %procedures, $proc_order, %views, $view_order );
58
59     sub _err {
60       my $max_lines = 5;
61       my @up_to_N_lines = split (/\n/, $_[1], $max_lines + 1);
62       die sprintf ("Unable to parse line %d:\n%s\n",
63         $_[0],
64         join "\n", (map { "'$_'" } @up_to_N_lines[0..$max_lines - 1 ]), @up_to_N_lines > $max_lines ? '...' : ()
65       );
66     }
67
68 }
69
70 startrule : statement(s) eofile
71    {
72       return {
73          tables     => \%tables,
74          procedures => \%procedures,
75          views      => \%views,
76       }
77    }
78
79 eofile : /^\Z/
80
81 statement : create_table
82     | create_procedure
83     | create_view
84     | create_index
85     | create_constraint
86     | comment
87     | drop
88     | use
89     | setuser
90     | if
91     | print
92     | grant
93     | exec
94     | /^\Z/ | { _err ($thisline, $text) }
95
96 use : /use/i WORD GO
97     { @table_comments = () }
98
99 setuser : /setuser/i NAME GO
100
101 if : /if/i object_not_null begin if_command end GO
102
103 if_command : grant
104     | create_index
105     | create_constraint
106
107 object_not_null : /object_id/i '(' ident ')' /is not null/i
108
109 print : /\s*/ /print/i /.*/
110
111 else : /else/i /.*/
112
113 begin : /begin/i
114
115 end : /end/i
116
117 grant : /grant/i /[^\n]*/
118
119 exec : exec_statement(s) GO
120
121 exec_statement : /exec/i /[^\n]+/
122
123 comment : /^\s*(?:#|-{2}).*\n/
124     {
125         my $comment =  $item[1];
126         $comment    =~ s/^\s*(#|--)\s*//;
127         $comment    =~ s/\s*$//;
128         $return     = $comment;
129         push @table_comments, $comment;
130     }
131
132 comment : comment_start comment_middle comment_end
133     {
134         my $comment = $item[2];
135         $comment =~ s/^\s*|\s*$//mg;
136         $comment =~ s/^\**\s*//mg;
137         push @table_comments, $comment;
138     }
139
140 comment_start : m#^\s*\/\*#
141
142 comment_end : m#\s*\*\/#
143
144 comment_middle : m{([^*]+|\*(?!/))*}
145
146 drop : if_exists(?) /drop/i tbl_drop END_STATEMENT
147
148 tbl_drop : /table/i NAME
149
150 if_exists : /if exists/i '(' /select/i 'name' /from/i 'sysobjects' /[^\)]+/ ')'
151
152 #
153 # Create table.
154 #
155 create_table : /create/i /table/i ident '(' create_def(s /,/) ')' lock(?) on_system(?) END_STATEMENT
156     {
157         my $table_owner = $item[3]{'owner'};
158         my $table_name  = $item[3]{'name'};
159
160         if ( @table_comments ) {
161             $tables{ $table_name }{'comments'} = [ @table_comments ];
162             @table_comments = ();
163         }
164
165         $tables{ $table_name }{'order'}  = ++$table_order;
166         $tables{ $table_name }{'name'}   = $table_name;
167         $tables{ $table_name }{'owner'}  = $table_owner;
168         $tables{ $table_name }{'system'} = $item[7];
169
170         my $i = 0;
171         for my $def ( @{ $item[5] } ) {
172             if ( $def->{'supertype'} eq 'field' ) {
173                 my $field_name = $def->{'name'};
174                 $tables{ $table_name }{'fields'}{ $field_name } =
175                     { %$def, order => $i };
176                 $i++;
177
178                 if ( $def->{'is_primary_key'} ) {
179                     push @{ $tables{ $table_name }{'constraints'} }, {
180                         type   => 'primary_key',
181                         fields => [ $field_name ],
182                     };
183                 }
184             }
185             elsif ( $def->{'supertype'} eq 'constraint' ) {
186                 push @{ $tables{ $table_name }{'constraints'} }, $def;
187             }
188             else {
189                 push @{ $tables{ $table_name }{'indices'} }, $def;
190             }
191         }
192     }
193
194 create_constraint : /create/i constraint
195     {
196         @table_comments = ();
197         push @{ $tables{ $item[2]{'table'} }{'constraints'} }, $item[2];
198     }
199
200 create_constraint : /alter/i /table/i ident /add/i foreign_key_constraint END_STATEMENT
201     {
202         push @{ $tables{ $item[3]{name} }{constraints} }, $item[5];
203     }
204
205 create_index : /create/i index
206     {
207         @table_comments = ();
208         push @{ $tables{ $item[2]{'table'} }{'indices'} }, $item[2];
209     }
210
211 create_procedure : /create/i PROCEDURE WORD not_go GO
212     {
213         @table_comments = ();
214         my $proc_name = $item[3];
215         my $owner = '';
216         my $sql = "$item[1] $item[2] $proc_name $item[4]";
217
218         $procedures{ $proc_name }{'order'}  = ++$proc_order;
219         $procedures{ $proc_name }{'name'}   = $proc_name;
220         $procedures{ $proc_name }{'owner'}  = $owner;
221         $procedures{ $proc_name }{'sql'}    = $sql;
222     }
223
224 create_procedure : /create/i PROCEDURE '[' WORD '].' WORD not_go GO
225     {
226         @table_comments = ();
227         my $proc_name = $item[6];
228         my $owner = $item[4];
229         my $sql = "$item[1] $item[2] [$owner].$proc_name $item[7]";
230
231         $procedures{ $proc_name }{'order'}  = ++$proc_order;
232         $procedures{ $proc_name }{'name'}   = $proc_name;
233         $procedures{ $proc_name }{'owner'}  = $owner;
234         $procedures{ $proc_name }{'sql'}    = $sql;
235     }
236
237 PROCEDURE : /procedure/i
238    | /function/i
239
240 create_view : /create/i /view/i WORD not_go GO
241     {
242         @table_comments = ();
243         my $view_name = $item[3];
244         my $sql = "$item[1] $item[2] $item[3] $item[4]";
245
246         $views{ $view_name }{'order'}  = ++$view_order;
247         $views{ $view_name }{'name'}   = $view_name;
248         $views{ $view_name }{'sql'}    = $sql;
249     }
250
251 not_go : /((?!\bgo\b).)*/is
252
253 create_def : constraint
254     | index
255     | field
256
257 blank : /\s*/
258
259 field : field_name data_type field_qualifier(s?)
260     {
261         my %qualifiers  = map { %$_ } @{ $item{'field_qualifier(s?)'} || [] };
262         my $nullable = defined $qualifiers{'nullable'}
263                    ? $qualifiers{'nullable'} : 1;
264         $return = {
265             supertype      => 'field',
266             name           => $item{'field_name'},
267             data_type      => $item{'data_type'}{'type'},
268             size           => $item{'data_type'}{'size'},
269             nullable       => $nullable,
270             default        => $qualifiers{'default_val'},
271             is_auto_inc    => $qualifiers{'is_auto_inc'},
272 #            is_primary_key => $item{'primary_key'}[0],
273         }
274     }
275
276 field_qualifier : nullable
277     {
278         $return = {
279              nullable => $item{'nullable'},
280         }
281     }
282
283 field_qualifier : default_val
284     {
285         $return = {
286              default_val => $item{'default_val'},
287         }
288     }
289
290 field_qualifier : auto_inc
291     {
292         $return = {
293              is_auto_inc => $item{'auto_inc'},
294         }
295     }
296
297 constraint : primary_key_constraint
298     | foreign_key_constraint
299     | unique_constraint
300
301 field_name : WORD
302
303 index_name : WORD
304
305 table_name : WORD
306
307 data_type : WORD field_size(?)
308     {
309         $return = {
310             type => $item[1],
311             size => $item[2][0]
312         }
313     }
314
315 lock : /lock/i /datarows/i
316
317 field_type : WORD
318
319 field_size : '(' num_range ')' { $item{'num_range'} }
320
321 num_range : DIGITS ',' DIGITS
322     { $return = $item[1].','.$item[3] }
323                | DIGITS
324     { $return = $item[1] }
325
326
327 nullable : /not/i /null/i
328     { $return = 0 }
329     | /null/i
330     { $return = 1 }
331
332 default_val : /default/i /null/i
333     { $return = 'null' }
334    | /default/i /'[^']*'/
335     { $item[2]=~ s/'//g; $return = $item[2] }
336    | /default/i WORD
337     { $return = $item[2] }
338
339 auto_inc : /identity/i { 1 }
340
341 primary_key_constraint : /constraint/i index_name(?) /primary/i /key/i parens_field_list
342     {
343         $return = {
344             supertype => 'constraint',
345             name      => $item[2][0],
346             type      => 'primary_key',
347             fields    => $item[5],
348         }
349     }
350
351 foreign_key_constraint : /constraint/i index_name(?) /foreign/i /key/i parens_field_list /references/i table_name parens_field_list(?) on_delete(?) on_update(?)
352     {
353         $return = {
354             supertype        => 'constraint',
355             name             => $item[2][0],
356             type             => 'foreign_key',
357             fields           => $item[5],
358             reference_table  => $item[7],
359             reference_fields => $item[8][0],
360             on_delete        => $item[9][0],
361             on_update        => $item[10][0],
362         }
363     }
364
365 unique_constraint : /constraint/i index_name(?) /unique/i parens_field_list
366     {
367         $return = {
368             supertype => 'constraint',
369             type      => 'unique',
370             name      => $item[2][0],
371             fields    => $item[4],
372         }
373     }
374
375 unique_constraint : /unique/i clustered(?) INDEX(?) index_name(?) on_table(?) parens_field_list
376     {
377         $return = {
378             supertype => 'constraint',
379             type      => 'unique',
380             clustered => $item[2][0],
381             name      => $item[4][0],
382             table     => $item[5][0],
383             fields    => $item[6],
384         }
385     }
386
387 on_delete : /on delete/i reference_option
388     { $item[2] }
389
390 on_update : /on update/i reference_option
391     { $item[2] }
392
393 reference_option: /cascade/i
394     { $item[1] }
395     | /no action/i
396     { $item[1] }
397
398 clustered : /clustered/i
399     { $return = 1 }
400     | /nonclustered/i
401     { $return = 0 }
402
403 INDEX : /index/i
404
405 on_table : /on/i table_name
406     { $return = $item[2] }
407
408 on_system : /on/i /system/i
409     { $return = 1 }
410
411 index : clustered(?) INDEX index_name(?) on_table(?) parens_field_list END_STATEMENT
412     {
413         $return = {
414             supertype => 'index',
415             type      => 'normal',
416             clustered => $item[1][0],
417             name      => $item[3][0],
418             table     => $item[4][0],
419             fields    => $item[5],
420         }
421     }
422
423 parens_field_list : '(' field_name(s /,/) ')'
424     { $item[2] }
425
426 ident : QUOTE(?) WORD '.' WORD QUOTE(?)
427     { $return = { owner => $item[2], name => $item[4] } }
428     | WORD
429     { $return = { name  => $item[1] } }
430
431 END_STATEMENT : ';'
432    | GO
433
434 GO : /^go/i
435
436 NAME : QUOTE(?) /\w+/ QUOTE(?)
437     { $item[2] }
438
439 WORD : /[\w#]+/
440
441 DIGITS : /\d+/
442
443 COMMA : ','
444
445 QUOTE : /'/
446
447 };
448
449 # -------------------------------------------------------------------
450 sub parse {
451     my ( $translator, $data ) = @_;
452     my $parser = Parse::RecDescent->new($GRAMMAR);
453
454     local $::RD_TRACE  = $translator->trace ? 1 : undef;
455     local $DEBUG       = $translator->debug;
456
457     unless (defined $parser) {
458         return $translator->error("Error instantiating Parse::RecDescent ".
459             "instance: Bad grammer");
460     }
461
462     my $result = $parser->startrule($data);
463     return $translator->error( "Parse failed." ) unless defined $result;
464     warn Dumper( $result ) if $DEBUG;
465
466     my $schema = $translator->schema;
467     my @tables = sort {
468         $result->{tables}->{ $a }->{'order'} <=> $result->{tables}->{ $b }->{'order'}
469     } keys %{ $result->{tables} };
470
471     for my $table_name ( @tables ) {
472         my $tdata = $result->{tables}->{ $table_name };
473         my $table = $schema->add_table( name => $tdata->{'name'} )
474                     or die "Can't create table '$table_name': ", $schema->error;
475
476         $table->comments( $tdata->{'comments'} );
477
478         my @fields = sort {
479             $tdata->{'fields'}->{$a}->{'order'}
480             <=>
481             $tdata->{'fields'}->{$b}->{'order'}
482         } keys %{ $tdata->{'fields'} };
483
484         for my $fname ( @fields ) {
485             my $fdata = $tdata->{'fields'}{ $fname };
486             my $field = $table->add_field(
487                 name              => $fdata->{'name'},
488                 data_type         => $fdata->{'data_type'},
489                 size              => $fdata->{'size'},
490                 default_value     => $fdata->{'default'},
491                 is_auto_increment => $fdata->{'is_auto_inc'},
492                 is_nullable       => $fdata->{'nullable'},
493                 comments          => $fdata->{'comments'},
494             ) or die $table->error;
495
496             $table->primary_key( $field->name ) if $fdata->{'is_primary_key'};
497
498             for my $qual ( qw[ binary unsigned zerofill list ] ) {
499                 if ( my $val = $fdata->{ $qual } || $fdata->{ uc $qual } ) {
500                     next if ref $val eq 'ARRAY' && !@$val;
501                     $field->extra( $qual, $val );
502                 }
503             }
504
505             if ( $field->data_type =~ /(set|enum)/i && !$field->size ) {
506                 my %extra = $field->extra;
507                 my $longest = 0;
508                 for my $len ( map { length } @{ $extra{'list'} || [] } ) {
509                     $longest = $len if $len > $longest;
510                 }
511                 $field->size( $longest ) if $longest;
512             }
513
514             for my $cdata ( @{ $fdata->{'constraints'} } ) {
515                 next unless $cdata->{'type'} eq 'foreign_key';
516                 $cdata->{'fields'} ||= [ $field->name ];
517                 push @{ $tdata->{'constraints'} }, $cdata;
518             }
519         }
520
521         for my $idata ( @{ $tdata->{'indices'} || [] } ) {
522             my $index  =  $table->add_index(
523                 name   => $idata->{'name'},
524                 type   => uc $idata->{'type'},
525                 fields => $idata->{'fields'},
526             ) or die $table->error;
527         }
528
529         for my $cdata ( @{ $tdata->{'constraints'} || [] } ) {
530             my $constraint       =  $table->add_constraint(
531                 name             => $cdata->{'name'},
532                 type             => $cdata->{'type'},
533                 fields           => $cdata->{'fields'},
534                 reference_table  => $cdata->{'reference_table'},
535                 reference_fields => $cdata->{'reference_fields'},
536                 match_type       => $cdata->{'match_type'} || '',
537                 on_delete        => $cdata->{'on_delete'} || $cdata->{'on_delete_do'},
538                 on_update        => $cdata->{'on_update'} || $cdata->{'on_update_do'},
539             ) or die $table->error;
540         }
541     }
542
543     my @procedures = sort {
544         $result->{procedures}->{ $a }->{'order'} <=> $result->{procedures}->{ $b }->{'order'}
545     } keys %{ $result->{procedures} };
546     for my $proc_name (@procedures) {
547       $schema->add_procedure(
548          name  => $proc_name,
549          owner => $result->{procedures}->{$proc_name}->{owner},
550          sql   => $result->{procedures}->{$proc_name}->{sql},
551       );
552     }
553
554     my @views = sort {
555         $result->{views}->{ $a }->{'order'} <=> $result->{views}->{ $b }->{'order'}
556     } keys %{ $result->{views} };
557     for my $view_name (keys %{ $result->{views} }) {
558       $schema->add_view(
559          name => $view_name,
560          sql  => $result->{views}->{$view_name}->{sql},
561       );
562     }
563
564     return 1;
565 }
566
567 1;
568
569 # -------------------------------------------------------------------
570 # Every hero becomes a bore at last.
571 # Ralph Waldo Emerson
572 # -------------------------------------------------------------------
573
574 =pod
575
576 =head1 AUTHOR
577
578 Chris Hilton E<lt>chris@dctank.comE<gt> - Bulk of code from
579 Sybase parser, I just tweaked it for SQLServer. Thanks.
580
581 =head1 SEE ALSO
582
583 SQL::Translator, SQL::Translator::Parser::DBI, L<http://www.midsomer.org/>.
584
585 =cut