1 package SQL::Translator::Producer::SQLServer;
3 # -------------------------------------------------------------------
4 # Copyright (C) 2002-2009 SQLFairy Authors
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.
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.
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
19 # -------------------------------------------------------------------
23 SQL::Translator::Producer::SQLServer - MS SQLServer producer for SQL::Translator
29 my $t = SQL::Translator->new( parser => '...', producer => 'SQLServer' );
34 B<WARNING>B This is still fairly early code, basically a hacked version of the
35 Sybase Producer (thanks Sam, Paul and Ken for doing the real work ;-)
37 =head1 Extra Attributes
43 List of values for an enum field.
49 * !! Write some tests !!
50 * Reserved words list needs updating to SQLServer.
51 * Triggers, Procedures and Views DO NOT WORK
56 use vars qw[ $DEBUG $WARN $VERSION ];
58 $DEBUG = 1 unless defined $DEBUG;
61 use SQL::Translator::Schema::Constants;
62 use SQL::Translator::Utils qw(debug header_comment);
68 #integer => 'numeric',
72 #varchar => 'varchar',
73 #varchar2 => 'varchar',
74 #timestamp => 'datetime',
76 #real => 'double precision',
79 #tinyint => 'smallint',
80 #float => 'double precision',
82 #boolean => 'varchar',
87 # TODO - This is still the Sybase list!
88 my %reserved = map { $_, 1 } qw[
89 ALL ANALYSE ANALYZE AND ANY AS ASC
91 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
92 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
93 DEFAULT DEFERRABLE DESC DISTINCT DO
95 FALSE FOR FOREIGN FREEZE FROM FULL
97 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
98 JOIN LEADING LEFT LIKE LIMIT
99 NATURAL NEW NOT NOTNULL NULL
100 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
101 PRIMARY PUBLIC REFERENCES RIGHT
102 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
103 UNION UNIQUE USER USING VERBOSE WHEN WHERE
106 # If these datatypes have size appended the sql fails.
107 my @no_size = qw/tinyint smallint int integer bigint text bit image datetime/;
109 my $max_id_length = 128;
114 =head1 SQLServer Create Table Syntax
120 # -------------------------------------------------------------------
122 my $translator = shift;
123 $DEBUG = $translator->debug;
124 $WARN = $translator->show_warnings;
125 my $no_comments = $translator->no_comments;
126 my $add_drop_table = $translator->add_drop_table;
127 my $schema = $translator->schema;
129 %global_names = (); #reset
132 $output .= header_comment."\n" unless ($no_comments);
134 # Generate the DROP statements. We do this in one block here as if we
135 # have fkeys we need to drop in the correct order otherwise they will fail
136 # due to the dependancies the fkeys setup. (There is no way to turn off
137 # fkey checking while we sort the schema like MySQL's set
138 # foreign_key_checks=0)
139 # We assume the tables are in the correct order to set them up as you need
140 # to have created a table to fkey to it. So the reverse order should drop
141 # them properly, fingers crossed...
142 if ($add_drop_table) {
143 $output .= "--\n-- Drop tables\n--\n\n" unless $no_comments;
145 sort { $b->order <=> $a->order } $schema->get_tables
147 my $name = unreserve($table->name);
148 $output .= qq{IF EXISTS (SELECT name FROM sysobjects WHERE name = '$name' AND type = 'U') DROP TABLE $name;\n\n}
152 # Generate the CREATE sql
154 my @foreign_constraints = (); # these need to be added separately, as tables may not exist yet
156 for my $table ( $schema->get_tables ) {
157 my $table_name = $table->name or next;
158 my $table_name_ur = unreserve($table_name) || '';
160 my ( @comments, @field_defs, @index_defs, @constraint_defs );
162 push @comments, "\n\n--\n-- Table: $table_name_ur\n--"
165 push @comments, map { "-- $_" } $table->comments;
170 my %field_name_scope;
171 for my $field ( $table->get_fields ) {
172 my $field_name = $field->name;
173 my $field_name_ur = unreserve( $field_name, $table_name );
174 my $field_def = qq["$field_name_ur"];
175 $field_def =~ s/\"//g;
176 if ( $field_def =~ /identity/ ){
177 $field_def =~ s/identity/pidentity/;
183 my $data_type = lc $field->data_type;
184 my $orig_data_type = $data_type;
185 my %extra = $field->extra;
186 my $list = $extra{'list'} || [];
187 # \todo deal with embedded quotes
188 my $commalist = join( ', ', map { qq['$_'] } @$list );
190 if ( $data_type eq 'enum' ) {
191 my $check_name = mk_name( $field_name . '_chk' );
192 push @constraint_defs,
193 "CONSTRAINT $check_name CHECK ($field_name IN ($commalist))";
194 $data_type .= 'character varying';
196 elsif ( $data_type eq 'set' ) {
197 $data_type .= 'character varying';
199 elsif ( grep { $data_type eq $_ } qw/bytea blob clob/ ) {
200 $data_type = 'varbinary';
203 if ( defined $translate{ $data_type } ) {
204 $data_type = $translate{ $data_type };
207 warn "Unknown datatype: $data_type ",
208 "($table_name.$field_name)\n" if $WARN;
212 my $size = $field->size;
213 if ( grep $_ eq $data_type, @no_size) {
214 # SQLServer doesn't seem to like sizes on some datatypes
218 if ( $data_type =~ /numeric/ ) {
221 elsif ( $orig_data_type eq 'text' ) {
222 #interpret text fields as long varchars
226 $data_type eq 'varchar' &&
227 $orig_data_type eq 'boolean'
231 elsif ( $data_type eq 'varchar' ) {
236 $field_def .= " $data_type";
237 $field_def .= "($size)" if $size;
239 $field_def .= ' IDENTITY' if $field->is_auto_increment;
242 # Not null constraint
244 unless ( $field->is_nullable ) {
245 $field_def .= ' NOT NULL';
248 $field_def .= ' NULL' if $data_type ne 'bit';
254 SQL::Translator::Producer->_apply_default_value(
262 push @field_defs, $field_def;
266 # Constraint Declarations
268 my @constraint_decs = ();
269 for my $constraint ( $table->get_constraints ) {
270 my $name = $constraint->name || '';
271 # Make sure we get a unique name
272 my $type = $constraint->type || NORMAL;
273 my @fields = map { unreserve( $_, $table_name ) }
275 my @rfields = map { unreserve( $_, $table_name ) }
276 $constraint->reference_fields;
280 if ( $type eq FOREIGN_KEY ) {
281 $name ||= mk_name( $table_name . '_fk' );
282 my $on_delete = uc ($constraint->on_delete || '');
283 my $on_update = uc ($constraint->on_update || '');
285 # The default implicit constraint action in MSSQL is RESTRICT
286 # but you can not specify it explicitly. Go figure :)
287 for ($on_delete, $on_update) {
288 undef $_ if $_ eq 'RESTRICT'
292 "ALTER TABLE $table_name ADD CONSTRAINT $name FOREIGN KEY".
293 ' (' . join( ', ', @fields ) . ') REFERENCES '.
294 $constraint->reference_table.
295 ' (' . join( ', ', @rfields ) . ')'
298 if ( $on_delete && $on_delete ne "NO ACTION") {
299 $c_def .= " ON DELETE $on_delete";
301 if ( $on_update && $on_update ne "NO ACTION") {
302 $c_def .= " ON UPDATE $on_update";
307 push @foreign_constraints, $c_def;
312 if ( $type eq PRIMARY_KEY ) {
313 $name ||= mk_name( $table_name . '_pk' );
315 "CONSTRAINT $name PRIMARY KEY ".
316 '(' . join( ', ', @fields ) . ')';
318 elsif ( $type eq UNIQUE ) {
319 $name ||= mk_name( $table_name . '_uc' );
321 "CONSTRAINT $name UNIQUE " .
322 '(' . join( ', ', @fields ) . ')';
324 push @constraint_defs, $c_def;
330 for my $index ( $table->get_indices ) {
331 my $idx_name = $index->name || mk_name($table_name . '_idx');
333 "CREATE INDEX $idx_name ON $table_name (".
334 join( ', ', $index->fields ) . ");";
337 my $create_statement = "";
338 $create_statement .= qq[CREATE TABLE $table_name_ur (\n].
340 map { " $_" } @field_defs, @constraint_defs
345 $output .= join( "\n\n",
353 $output .= join ("\n", '', @foreign_constraints) if @foreign_constraints;
355 # create view/procedure are NOT prepended to the input $sql, needs
356 # to be filled in with the proper syntax
360 # Text of view is already a 'create view' statement so no need to
362 foreach ( $schema->get_views ) {
363 my $name = $_->name();
365 $output .= "--\n-- View: $name\n--\n\n" unless $no_comments;
366 my $text = $_->sql();
368 $output .= "$text\nGO\n";
371 # Text of procedure already has the 'create procedure' stuff
372 # so there is no need to do anything fancy. However, we should
373 # think about doing fancy stuff with granting permissions and
375 foreach ( $schema->get_procedures ) {
376 my $name = $_->name();
378 $output .= "--\n-- Procedure: $name\n--\n\n" unless $no_comments;
379 my $text = $_->sql();
381 $output .= "$text\nGO\n";
388 # -------------------------------------------------------------------
390 my ($name, $scope, $critical) = @_;
392 $scope ||= \%global_names;
393 if ( my $prev = $scope->{ $name } ) {
394 my $name_orig = $name;
395 $name .= sprintf( "%02d", ++$prev );
396 substr($name, $max_id_length - 3) = "00"
397 if length( $name ) > $max_id_length;
399 warn "The name '$name_orig' has been changed to ",
400 "'$name' to make it unique.\n" if $WARN;
402 $scope->{ $name_orig }++;
404 $name = substr( $name, 0, $max_id_length )
405 if ((length( $name ) > $max_id_length) && $critical);
410 # -------------------------------------------------------------------
412 my $name = shift || '';
413 my $schema_obj_name = shift || '';
414 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
416 # also trap fields that don't begin with a letter
417 return $name if !$reserved{ uc $name } && $name =~ /^[a-z]/i;
419 my $unreserve = sprintf '%s_', $name;
420 return $unreserve.$suffix;
425 # -------------------------------------------------------------------
435 Mark Addison E<lt>grommit@users.sourceforge.netE<gt> - Bulk of code from
436 Sybase producer, I just tweaked it for SQLServer. Thanks.