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