1 package SQL::Translator::Producer::SQLServer;
3 # -------------------------------------------------------------------
4 # $Id: SQLServer.pm,v 1.7 2007-03-14 16:56:33 duality72 Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-4 SQLFairy Authors
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation; version 2.
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21 # -------------------------------------------------------------------
25 SQL::Translator::Producer::SQLServer - MS SQLServer producer for SQL::Translator
31 my $t = SQL::Translator->new( parser => '...', producer => 'SQLServer' );
36 B<WARNING>B This is still fairly early code, basically a hacked version of the
37 Sybase Producer (thanks Sam, Paul and Ken for doing the real work ;-)
39 =head1 Extra Attributes
45 List of values for an enum field.
51 * !! Write some tests !!
52 * Reserved words list needs updating to SQLServer.
53 * Triggers, Procedures and Views havn't been tested at all.
58 use vars qw[ $DEBUG $WARN $VERSION ];
59 $VERSION = sprintf "%d.%02d", q$Revision: 1.7 $ =~ /(\d+)\.(\d+)/;
60 $DEBUG = 1 unless defined $DEBUG;
63 use SQL::Translator::Schema::Constants;
64 use SQL::Translator::Utils qw(debug header_comment);
70 #integer => 'numeric',
74 #varchar => 'varchar',
75 #varchar2 => 'varchar',
76 #timestamp => 'datetime',
78 #real => 'double precision',
81 #tinyint => 'smallint',
82 #float => 'double precision',
84 #boolean => 'varchar',
89 # TODO - This is still the Sybase list!
90 my %reserved = map { $_, 1 } qw[
91 ALL ANALYSE ANALYZE AND ANY AS ASC
93 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
94 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
95 DEFAULT DEFERRABLE DESC DISTINCT DO
97 FALSE FOR FOREIGN FREEZE FROM FULL
99 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
100 JOIN LEADING LEFT LIKE LIMIT
101 NATURAL NEW NOT NOTNULL NULL
102 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
103 PRIMARY PUBLIC REFERENCES RIGHT
104 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
105 UNION UNIQUE USER USING VERBOSE WHEN WHERE
108 # If these datatypes have size appended the sql fails.
109 my @no_size = qw/tinyint smallint int integer bigint text bit image datetime/;
111 my $max_id_length = 128;
112 my %used_identifiers = ();
119 =head1 SQLServer Create Table Syntax
125 # -------------------------------------------------------------------
127 my $translator = shift;
128 $DEBUG = $translator->debug;
129 $WARN = $translator->show_warnings;
130 my $no_comments = $translator->no_comments;
131 my $add_drop_table = $translator->add_drop_table;
132 my $schema = $translator->schema;
135 $output .= header_comment."\n" unless ($no_comments);
137 # Generate the DROP statements. We do this in one block here as if we
138 # have fkeys we need to drop in the correct order otherwise they will fail
139 # due to the dependancies the fkeys setup. (There is no way to turn off
140 # fkey checking while we sort the schema like MySQL's set
141 # foreign_key_checks=0)
142 # We assume the tables are in the correct order to set them up as you need
143 # to have created a table to fkey to it. So the reverse order should drop
144 # them properly, fingers crossed...
145 if ($add_drop_table) {
146 $output .= "--\n-- Drop tables\n--\n\n" unless $no_comments;
148 sort { $b->order <=> $a->order } $schema->get_tables
150 my $name = unreserve($table->name);
151 $output .= qq{IF EXISTS (SELECT name FROM sysobjects WHERE name = '$name' AND type = 'U') DROP TABLE $name;\n\n}
155 # Generate the CREATE sql
156 for my $table ( $schema->get_tables ) {
157 my $table_name = $table->name or next;
158 $table_name = mk_name( $table_name, '', undef, 1 );
159 my $table_name_ur = unreserve($table_name) || '';
161 my ( @comments, @field_defs, @index_defs, @constraint_defs );
163 push @comments, "\n\n--\n-- Table: $table_name_ur\n--"
166 push @comments, map { "-- $_" } $table->comments;
171 my %field_name_scope;
172 for my $field ( $table->get_fields ) {
173 my $field_name = mk_name(
174 $field->name, '', \%field_name_scope, undef,1
176 my $field_name_ur = unreserve( $field_name, $table_name );
177 my $field_def = qq["$field_name_ur"];
178 $field_def =~ s/\"//g;
179 if ( $field_def =~ /identity/ ){
180 $field_def =~ s/identity/pidentity/;
186 my $data_type = lc $field->data_type;
187 my $orig_data_type = $data_type;
188 my %extra = $field->extra;
189 my $list = $extra{'list'} || [];
190 # \todo deal with embedded quotes
191 my $commalist = join( ', ', map { qq['$_'] } @$list );
193 if ( $data_type eq 'enum' ) {
194 my $check_name = mk_name(
195 $table_name.'_'.$field_name, 'chk' ,undef, 1
197 push @constraint_defs,
198 "CONSTRAINT $check_name CHECK ($field_name IN ($commalist))";
199 $data_type .= 'character varying';
201 elsif ( $data_type eq 'set' ) {
202 $data_type .= 'character varying';
205 if ( defined $translate{ $data_type } ) {
206 $data_type = $translate{ $data_type };
209 warn "Unknown datatype: $data_type ",
210 "($table_name.$field_name)\n" if $WARN;
214 my $size = $field->size;
215 if ( grep $_ eq $data_type, @no_size) {
216 # SQLServer doesn't seem to like sizes on some datatypes
220 if ( $data_type =~ /numeric/ ) {
223 elsif ( $orig_data_type eq 'text' ) {
224 #interpret text fields as long varchars
228 $data_type eq 'varchar' &&
229 $orig_data_type eq 'boolean'
233 elsif ( $data_type eq 'varchar' ) {
238 $field_def .= " $data_type";
239 $field_def .= "($size)" if $size;
241 $field_def .= ' IDENTITY' if $field->is_auto_increment;
244 # Not null constraint
246 unless ( $field->is_nullable ) {
247 $field_def .= ' NOT NULL';
250 $field_def .= ' NULL' if $data_type ne 'bit';
256 my $default = $field->default_value;
257 if ( defined $default ) {
258 SQL::Translator::Producer->_apply_default_value(
267 push @field_defs, $field_def;
271 # Constraint Declarations
273 my @constraint_decs = ();
275 for my $constraint ( $table->get_constraints ) {
276 my $name = $constraint->name || '';
277 # Make sure we get a unique name
278 $name = mk_name( $name, undef, undef, 1 ) if $name;
279 my $type = $constraint->type || NORMAL;
280 my @fields = map { unreserve( $_, $table_name ) }
282 my @rfields = map { unreserve( $_, $table_name ) }
283 $constraint->reference_fields;
287 if ( $type eq PRIMARY_KEY ) {
288 $name ||= mk_name( $table_name, 'pk', undef,1 );
290 "CONSTRAINT $name PRIMARY KEY ".
291 '(' . join( ', ', @fields ) . ')';
293 elsif ( $type eq FOREIGN_KEY ) {
294 $name ||= mk_name( $table_name, 'fk', undef,1 );
295 #$name = mk_name( ($name || $table_name), 'fk', undef,1 );
297 "CONSTRAINT $name FOREIGN KEY".
298 ' (' . join( ', ', @fields ) . ') REFERENCES '.
299 $constraint->reference_table.
300 ' (' . join( ', ', @rfields ) . ')';
301 my $on_delete = $constraint->on_delete;
302 if ( defined $on_delete && $on_delete ne "NO ACTION") {
303 $c_def .= " ON DELETE $on_delete";
305 my $on_update = $constraint->on_update;
306 if ( defined $on_update && $on_update ne "NO ACTION") {
307 $c_def .= " ON UPDATE $on_update";
310 elsif ( $type eq UNIQUE ) {
313 $name || ++$c_name_default,undef, 1
316 "CONSTRAINT $name UNIQUE " .
317 '(' . join( ', ', @fields ) . ')';
319 push @constraint_defs, $c_def;
325 for my $index ( $table->get_indices ) {
326 my $idx_name = $index->name || mk_name($table_name,'idx',undef,1);
328 "CREATE INDEX $idx_name ON $table_name (".
329 join( ', ', $index->fields ) . ");";
332 my $create_statement = "";
333 $create_statement .= qq[CREATE TABLE $table_name_ur (\n].
335 map { " $_" } @field_defs, @constraint_defs
340 $output .= join( "\n\n",
348 # Text of view is already a 'create view' statement so no need to
350 foreach ( $schema->get_views ) {
351 my $name = $_->name();
353 $output .= "--\n-- View: $name\n--\n\n" unless $no_comments;
354 my $text = $_->sql();
356 $output .= "$text\nGO\n";
359 # Text of procedure already has the 'create procedure' stuff
360 # so there is no need to do anything fancy. However, we should
361 # think about doing fancy stuff with granting permissions and
363 foreach ( $schema->get_procedures ) {
364 my $name = $_->name();
366 $output .= "--\n-- Procedure: $name\n--\n\n" unless $no_comments;
367 my $text = $_->sql();
369 $output .= "$text\nGO\n";
372 # Warn out how we messed with the names.
375 warn "Truncated " . keys( %truncated ) . " names:\n";
376 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
379 warn "Encounted " . keys( %unreserve ) .
380 " unsafe names in schema (reserved or invalid):\n";
381 warn "\t" . join( "\n\t", sort keys %unreserve ) . "\n";
388 # -------------------------------------------------------------------
390 my $basename = shift || '';
391 my $type = shift || '';
392 my $scope = shift || '';
393 my $critical = shift || '';
394 my $basename_orig = $basename;
396 ? $max_id_length - (length($type) + 1)
398 $basename = substr( $basename, 0, $max_name )
399 if length( $basename ) > $max_name;
400 my $name = $type ? "${type}_$basename" : $basename;
402 if ( $basename ne $basename_orig and $critical ) {
403 my $show_type = $type ? "+'$type'" : "";
404 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
405 "character limit to make '$name'\n" if $WARN;
406 $truncated{ $basename_orig } = $name;
409 $scope ||= \%global_names;
410 if ( my $prev = $scope->{ $name } ) {
411 my $name_orig = $name;
412 $name .= sprintf( "%02d", ++$prev );
413 substr($name, $max_id_length - 3) = "00"
414 if length( $name ) > $max_id_length;
416 warn "The name '$name_orig' has been changed to ",
417 "'$name' to make it unique.\n" if $WARN;
419 $scope->{ $name_orig }++;
421 $name = substr( $name, 0, $max_id_length )
422 if ((length( $name ) > $max_id_length) && $critical);
427 # -------------------------------------------------------------------
429 my $name = shift || '';
430 my $schema_obj_name = shift || '';
431 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
433 # also trap fields that don't begin with a letter
434 return $name if !$reserved{ uc $name } && $name =~ /^[a-z]/i;
436 if ( $schema_obj_name ) {
437 ++$unreserve{"$schema_obj_name.$name"};
440 ++$unreserve{"$name (table name)"};
443 my $unreserve = sprintf '%s_', $name;
444 return $unreserve.$suffix;
449 # -------------------------------------------------------------------
459 Mark Addison E<lt>grommit@users.sourceforge.netE<gt> - Bulk of code from
460 Sybase producer, I just tweaked it for SQLServer. Thanks.