whitespace changes
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Parser / SQLServer.pm
CommitLineData
be4469ab 1package SQL::Translator::Parser::SQLServer;
2
44659089 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
be4469ab 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 ];
11ad2df9 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
e5f65c59 56{
ff3dd529 57 my ( %tables, @table_comments, $table_order, %procedures, $proc_order, %views, $view_order );
28224573 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
be4469ab 68}
69
ff3dd529 70startrule : statement(s) eofile
e5f65c59 71 {
72 return {
73 tables => \%tables,
74 procedures => \%procedures,
75 views => \%views,
76 }
77 }
be4469ab 78
79eofile : /^\Z/
80
81statement : create_table
82 | create_procedure
ff3dd529 83 | create_view
be4469ab 84 | create_index
85 | create_constraint
86 | comment
e2fb9ad3 87 | drop
be4469ab 88 | use
89 | setuser
90 | if
91 | print
92 | grant
93 | exec
28224573 94 | /^\Z/ | { _err ($thisline, $text) }
be4469ab 95
e5f65c59 96use : /use/i WORD GO
be4469ab 97 { @table_comments = () }
98
99setuser : /setuser/i NAME GO
100
101if : /if/i object_not_null begin if_command end GO
102
103if_command : grant
104 | create_index
105 | create_constraint
106
107object_not_null : /object_id/i '(' ident ')' /is not null/i
108
109print : /\s*/ /print/i /.*/
110
111else : /else/i /.*/
112
113begin : /begin/i
114
115end : /end/i
116
117grant : /grant/i /[^\n]*/
118
119exec : exec_statement(s) GO
120
121exec_statement : /exec/i /[^\n]+/
122
e5f65c59 123comment : /^\s*(?:#|-{2}).*\n/
124 {
be4469ab 125 my $comment = $item[1];
126 $comment =~ s/^\s*(#|--)\s*//;
127 $comment =~ s/\s*$//;
128 $return = $comment;
129 push @table_comments, $comment;
130 }
131
132comment : comment_start comment_middle comment_end
e5f65c59 133 {
be4469ab 134 my $comment = $item[2];
135 $comment =~ s/^\s*|\s*$//mg;
136 $comment =~ s/^\**\s*//mg;
137 push @table_comments, $comment;
138 }
139
140comment_start : m#^\s*\/\*#
141
142comment_end : m#\s*\*\/#
143
144comment_middle : m{([^*]+|\*(?!/))*}
145
e2fb9ad3 146drop : if_exists(?) /drop/i tbl_drop END_STATEMENT
147
148tbl_drop : /table/i NAME
149
150if_exists : /if exists/i '(' /select/i 'name' /from/i 'sysobjects' /[^\)]+/ ')'
151
be4469ab 152#
153# Create table.
154#
ff3dd529 155create_table : /create/i /table/i ident '(' create_def(s /,/) ')' lock(?) on_system(?) END_STATEMENT
e5f65c59 156 {
be4469ab 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'};
e5f65c59 174 $tables{ $table_name }{'fields'}{ $field_name } =
be4469ab 175 { %$def, order => $i };
176 $i++;
e5f65c59 177
be4469ab 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
f9a5ee79 194create_constraint : /create/i constraint
be4469ab 195 {
196 @table_comments = ();
197 push @{ $tables{ $item[2]{'table'} }{'constraints'} }, $item[2];
198 }
199
f9a5ee79 200create_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
be4469ab 205create_index : /create/i index
206 {
207 @table_comments = ();
208 push @{ $tables{ $item[2]{'table'} }{'indices'} }, $item[2];
209 }
210
ff3dd529 211create_procedure : /create/i PROCEDURE WORD not_go GO
be4469ab 212 {
213 @table_comments = ();
ff3dd529 214 my $proc_name = $item[3];
215 my $owner = '';
216 my $sql = "$item[1] $item[2] $proc_name $item[4]";
e5f65c59 217
ff3dd529 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;
be4469ab 222 }
223
ff3dd529 224create_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]";
e5f65c59 230
ff3dd529 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
237PROCEDURE : /procedure/i
e5f65c59 238 | /function/i
ff3dd529 239
ff3dd529 240create_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]";
e5f65c59 245
ff3dd529 246 $views{ $view_name }{'order'} = ++$view_order;
247 $views{ $view_name }{'name'} = $view_name;
248 $views{ $view_name }{'sql'} = $sql;
249 }
250
ff3dd529 251not_go : /((?!\bgo\b).)*/is
be4469ab 252
253create_def : constraint
254 | index
255 | field
256
257blank : /\s*/
258
259field : field_name data_type field_qualifier(s?)
e5f65c59 260 {
be4469ab 261 my %qualifiers = map { %$_ } @{ $item{'field_qualifier(s?)'} || [] };
e5f65c59 262 my $nullable = defined $qualifiers{'nullable'}
be4469ab 263 ? $qualifiers{'nullable'} : 1;
e5f65c59 264 $return = {
be4469ab 265 supertype => 'field',
e5f65c59 266 name => $item{'field_name'},
be4469ab 267 data_type => $item{'data_type'}{'type'},
268 size => $item{'data_type'}{'size'},
e5f65c59 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 }
be4469ab 274 }
275
276field_qualifier : nullable
e5f65c59 277 {
278 $return = {
be4469ab 279 nullable => $item{'nullable'},
e5f65c59 280 }
be4469ab 281 }
282
283field_qualifier : default_val
e5f65c59 284 {
285 $return = {
be4469ab 286 default_val => $item{'default_val'},
e5f65c59 287 }
be4469ab 288 }
289
290field_qualifier : auto_inc
e5f65c59 291 {
292 $return = {
be4469ab 293 is_auto_inc => $item{'auto_inc'},
e5f65c59 294 }
be4469ab 295 }
296
297constraint : primary_key_constraint
298 | foreign_key_constraint
299 | unique_constraint
300
301field_name : WORD
302
303index_name : WORD
304
305table_name : WORD
306
e5f65c59 307data_type : WORD field_size(?)
308 {
309 $return = {
310 type => $item[1],
be4469ab 311 size => $item[2][0]
e5f65c59 312 }
be4469ab 313 }
314
315lock : /lock/i /datarows/i
316
317field_type : WORD
318
319field_size : '(' num_range ')' { $item{'num_range'} }
320
321num_range : DIGITS ',' DIGITS
322 { $return = $item[1].','.$item[3] }
323 | DIGITS
324 { $return = $item[1] }
325
326
327nullable : /not/i /null/i
328 { $return = 0 }
329 | /null/i
330 { $return = 1 }
331
2a8fb466 332default_val : /default/i /null/i
333 { $return = 'null' }
e5f65c59 334 | /default/i /'[^']*'/
be4469ab 335 { $item[2]=~ s/'//g; $return = $item[2] }
e5f65c59 336 | /default/i WORD
06baeb21 337 { $return = $item[2] }
be4469ab 338
339auto_inc : /identity/i { 1 }
340
341primary_key_constraint : /constraint/i index_name(?) /primary/i /key/i parens_field_list
e5f65c59 342 {
343 $return = {
be4469ab 344 supertype => 'constraint',
345 name => $item[2][0],
346 type => 'primary_key',
347 fields => $item[5],
e5f65c59 348 }
be4469ab 349 }
350
100684f3 351foreign_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 352 {
e5f65c59 353 $return = {
be4469ab 354 supertype => 'constraint',
355 name => $item[2][0],
356 type => 'foreign_key',
357 fields => $item[5],
358 reference_table => $item[7],
e5f65c59 359 reference_fields => $item[8][0],
100684f3 360 on_delete => $item[9][0],
361 on_update => $item[10][0],
e5f65c59 362 }
be4469ab 363 }
364
e2fb9ad3 365unique_constraint : /constraint/i index_name(?) /unique/i parens_field_list
366 {
e5f65c59 367 $return = {
e2fb9ad3 368 supertype => 'constraint',
369 type => 'unique',
370 name => $item[2][0],
371 fields => $item[4],
372 }
373 }
374
be4469ab 375unique_constraint : /unique/i clustered(?) INDEX(?) index_name(?) on_table(?) parens_field_list
e5f65c59 376 {
377 $return = {
be4469ab 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],
e5f65c59 384 }
be4469ab 385 }
386
100684f3 387on_delete : /on delete/i reference_option
be4469ab 388 { $item[2] }
389
100684f3 390on_update : /on update/i reference_option
be4469ab 391 { $item[2] }
392
ff3dd529 393reference_option: /cascade/i
394 { $item[1] }
395 | /no action/i
396 { $item[1] }
be4469ab 397
398clustered : /clustered/i
399 { $return = 1 }
400 | /nonclustered/i
401 { $return = 0 }
402
403INDEX : /index/i
404
405on_table : /on/i table_name
406 { $return = $item[2] }
407
408on_system : /on/i /system/i
409 { $return = 1 }
410
e2fb9ad3 411index : clustered(?) INDEX index_name(?) on_table(?) parens_field_list END_STATEMENT
e5f65c59 412 {
413 $return = {
be4469ab 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],
e5f65c59 420 }
be4469ab 421 }
422
423parens_field_list : '(' field_name(s /,/) ')'
424 { $item[2] }
425
426ident : QUOTE(?) WORD '.' WORD QUOTE(?)
427 { $return = { owner => $item[2], name => $item[4] } }
428 | WORD
429 { $return = { name => $item[1] } }
430
ff3dd529 431END_STATEMENT : ';'
e5f65c59 432 | GO
ff3dd529 433
be4469ab 434GO : /^go/i
435
436NAME : QUOTE(?) /\w+/ QUOTE(?)
437 { $item[2] }
438
439WORD : /[\w#]+/
440
441DIGITS : /\d+/
442
443COMMA : ','
444
445QUOTE : /'/
446
447};
448
449# -------------------------------------------------------------------
450sub 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;
e5f65c59 467 my @tables = sort {
ff3dd529 468 $result->{tables}->{ $a }->{'order'} <=> $result->{tables}->{ $b }->{'order'}
469 } keys %{ $result->{tables} };
be4469ab 470
471 for my $table_name ( @tables ) {
ff3dd529 472 my $tdata = $result->{tables}->{ $table_name };
e5f65c59 473 my $table = $schema->add_table( name => $tdata->{'name'} )
be4469ab 474 or die "Can't create table '$table_name': ", $schema->error;
475
476 $table->comments( $tdata->{'comments'} );
477
e5f65c59 478 my @fields = sort {
479 $tdata->{'fields'}->{$a}->{'order'}
be4469ab 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'} || '',
100684f3 537 on_delete => $cdata->{'on_delete'} || $cdata->{'on_delete_do'},
538 on_update => $cdata->{'on_update'} || $cdata->{'on_update_do'},
be4469ab 539 ) or die $table->error;
540 }
541 }
e5f65c59 542
543 my @procedures = sort {
ff3dd529 544 $result->{procedures}->{ $a }->{'order'} <=> $result->{procedures}->{ $b }->{'order'}
545 } keys %{ $result->{procedures} };
546 for my $proc_name (@procedures) {
e5f65c59 547 $schema->add_procedure(
548 name => $proc_name,
549 owner => $result->{procedures}->{$proc_name}->{owner},
550 sql => $result->{procedures}->{$proc_name}->{sql},
551 );
ff3dd529 552 }
553
e5f65c59 554 my @views = sort {
ff3dd529 555 $result->{views}->{ $a }->{'order'} <=> $result->{views}->{ $b }->{'order'}
556 } keys %{ $result->{views} };
557 for my $view_name (keys %{ $result->{views} }) {
e5f65c59 558 $schema->add_view(
559 name => $view_name,
560 sql => $result->{views}->{$view_name}->{sql},
561 );
ff3dd529 562 }
be4469ab 563
564 return 1;
565}
566
5671;
568
569# -------------------------------------------------------------------
570# Every hero becomes a bore at last.
571# Ralph Waldo Emerson
572# -------------------------------------------------------------------
573
574=pod
575
576=head1 AUTHOR
577
578Chris Hilton E<lt>chris@dctank.comE<gt> - Bulk of code from
579Sybase parser, I just tweaked it for SQLServer. Thanks.
580
581=head1 SEE ALSO
582
583SQL::Translator, SQL::Translator::Parser::DBI, L<http://www.midsomer.org/>.
584
585=cut