1 package SQL::Translator::Producer::SQLServer;
5 SQL::Translator::Producer::SQLServer - MS SQLServer producer for SQL::Translator
11 my $t = SQL::Translator->new( parser => '...', producer => 'SQLServer' );
16 B<WARNING>B This is still fairly early code, basically a hacked version of the
17 Sybase Producer (thanks Sam, Paul and Ken for doing the real work ;-)
19 =head1 Extra Attributes
25 List of values for an enum field.
31 * !! Write some tests !!
32 * Reserved words list needs updating to SQLServer.
33 * Triggers, Procedures and Views DO NOT WORK
38 use vars qw[ $DEBUG $WARN $VERSION ];
40 $DEBUG = 1 unless defined $DEBUG;
43 use SQL::Translator::Schema::Constants;
44 use SQL::Translator::Utils qw(debug header_comment);
50 #integer => 'numeric',
54 #varchar => 'varchar',
55 #varchar2 => 'varchar',
56 #timestamp => 'datetime',
58 #real => 'double precision',
61 #tinyint => 'smallint',
62 #float => 'double precision',
64 #boolean => 'varchar',
69 # TODO - This is still the Sybase list!
70 my %reserved = map { $_, 1 } qw[
71 ALL ANALYSE ANALYZE AND ANY AS ASC
73 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
74 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
75 DEFAULT DEFERRABLE DESC DISTINCT DO
77 FALSE FOR FOREIGN FREEZE FROM FULL
79 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
80 JOIN LEADING LEFT LIKE LIMIT
81 NATURAL NEW NOT NOTNULL NULL
82 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
83 PRIMARY PUBLIC REFERENCES RIGHT
84 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
85 UNION UNIQUE USER USING VERBOSE WHEN WHERE
88 # If these datatypes have size appended the sql fails.
89 my @no_size = qw/tinyint smallint int integer bigint text bit image datetime/;
91 my $max_id_length = 128;
97 =head1 SQLServer Create Table Syntax
103 # -------------------------------------------------------------------
105 my $translator = shift;
106 $DEBUG = $translator->debug;
107 $WARN = $translator->show_warnings;
108 my $no_comments = $translator->no_comments;
109 my $add_drop_table = $translator->add_drop_table;
110 my $schema = $translator->schema;
112 %global_names = (); #reset
116 $output .= header_comment."\n" unless ($no_comments);
118 # Generate the DROP statements. We do this in one block here as if we
119 # have fkeys we need to drop in the correct order otherwise they will fail
120 # due to the dependancies the fkeys setup. (There is no way to turn off
121 # fkey checking while we sort the schema like MySQL's set
122 # foreign_key_checks=0)
123 # We assume the tables are in the correct order to set them up as you need
124 # to have created a table to fkey to it. So the reverse order should drop
125 # them properly, fingers crossed...
126 if ($add_drop_table) {
127 $output .= "--\n-- Drop tables\n--\n\n" unless $no_comments;
129 sort { $b->order <=> $a->order } $schema->get_tables
131 my $name = unreserve($table->name);
132 $output .= qq{IF EXISTS (SELECT name FROM sysobjects WHERE name = '$name' AND type = 'U') DROP TABLE $name;\n\n}
136 # Generate the CREATE sql
138 my @foreign_constraints = (); # these need to be added separately, as tables may not exist yet
140 for my $table ( $schema->get_tables ) {
141 my $table_name = $table->name or next;
142 my $table_name_ur = unreserve($table_name) || '';
144 my ( @comments, @field_defs, @index_defs, @constraint_defs );
146 push @comments, "\n\n--\n-- Table: $table_name_ur\n--"
149 push @comments, map { "-- $_" } $table->comments;
154 my %field_name_scope;
155 for my $field ( $table->get_fields ) {
156 my $field_name = $field->name;
157 my $field_name_ur = unreserve( $field_name, $table_name );
158 my $field_def = qq["$field_name_ur"];
159 $field_def =~ s/\"//g;
160 if ( $field_def =~ /identity/ ){
161 $field_def =~ s/identity/pidentity/;
167 my $data_type = lc $field->data_type;
168 my $orig_data_type = $data_type;
169 my %extra = $field->extra;
170 my $list = $extra{'list'} || [];
171 # \todo deal with embedded quotes
172 my $commalist = join( ', ', map { qq['$_'] } @$list );
174 if ( $data_type eq 'enum' ) {
175 my $check_name = mk_name( $field_name . '_chk' );
176 push @constraint_defs,
177 "CONSTRAINT $check_name CHECK ($field_name IN ($commalist))";
178 $data_type .= 'character varying';
180 elsif ( $data_type eq 'set' ) {
181 $data_type .= 'character varying';
183 elsif ( grep { $data_type eq $_ } qw/bytea blob clob/ ) {
184 $data_type = 'varbinary';
187 if ( defined $translate{ $data_type } ) {
188 $data_type = $translate{ $data_type };
191 warn "Unknown datatype: $data_type ",
192 "($table_name.$field_name)\n" if $WARN;
196 my $size = $field->size;
197 if ( grep $_ eq $data_type, @no_size) {
198 # SQLServer doesn't seem to like sizes on some datatypes
202 if ( $data_type =~ /numeric/ ) {
205 elsif ( $orig_data_type eq 'text' ) {
206 #interpret text fields as long varchars
210 $data_type eq 'varchar' &&
211 $orig_data_type eq 'boolean'
215 elsif ( $data_type eq 'varchar' ) {
220 $field_def .= " $data_type";
221 $field_def .= "($size)" if $size;
223 $field_def .= ' IDENTITY' if $field->is_auto_increment;
226 # Not null constraint
228 unless ( $field->is_nullable ) {
229 $field_def .= ' NOT NULL';
232 $field_def .= ' NULL' if $data_type ne 'bit';
238 SQL::Translator::Producer->_apply_default_value(
246 push @field_defs, $field_def;
250 # Constraint Declarations
252 my @constraint_decs = ();
253 for my $constraint ( $table->get_constraints ) {
254 my $name = $constraint->name || '';
255 # Make sure we get a unique name
256 my $type = $constraint->type || NORMAL;
257 my @fields = map { unreserve( $_, $table_name ) }
259 my @rfields = map { unreserve( $_, $table_name ) }
260 $constraint->reference_fields;
264 if ( $type eq FOREIGN_KEY ) {
265 $name ||= mk_name( $table_name . '_fk' );
266 my $on_delete = uc ($constraint->on_delete || '');
267 my $on_update = uc ($constraint->on_update || '');
269 # The default implicit constraint action in MSSQL is RESTRICT
270 # but you can not specify it explicitly. Go figure :)
271 for ($on_delete, $on_update) {
272 undef $_ if $_ eq 'RESTRICT'
276 "ALTER TABLE $table_name ADD CONSTRAINT $name FOREIGN KEY".
277 ' (' . join( ', ', @fields ) . ') REFERENCES '.
278 $constraint->reference_table.
279 ' (' . join( ', ', @rfields ) . ')'
282 if ( $on_delete && $on_delete ne "NO ACTION") {
283 $c_def .= " ON DELETE $on_delete";
285 if ( $on_update && $on_update ne "NO ACTION") {
286 $c_def .= " ON UPDATE $on_update";
291 push @foreign_constraints, $c_def;
296 if ( $type eq PRIMARY_KEY ) {
297 $name ||= mk_name( $table_name . '_pk' );
299 "CONSTRAINT $name PRIMARY KEY ".
300 '(' . join( ', ', @fields ) . ')';
302 elsif ( $type eq UNIQUE ) {
303 $name ||= mk_name( $table_name . '_uc' );
305 "CONSTRAINT $name UNIQUE " .
306 '(' . join( ', ', @fields ) . ')';
308 push @constraint_defs, $c_def;
314 for my $index ( $table->get_indices ) {
315 my $idx_name = $index->name || mk_name($table_name . '_idx');
317 "CREATE INDEX $idx_name ON $table_name (".
318 join( ', ', $index->fields ) . ");";
321 my $create_statement = "";
322 $create_statement .= qq[CREATE TABLE $table_name_ur (\n].
324 map { " $_" } @field_defs, @constraint_defs
329 $output .= join( "\n\n",
337 $output .= join ("\n", '', @foreign_constraints) if @foreign_constraints;
339 # create view/procedure are NOT prepended to the input $sql, needs
340 # to be filled in with the proper syntax
344 # Text of view is already a 'create view' statement so no need to
346 foreach ( $schema->get_views ) {
347 my $name = $_->name();
349 $output .= "--\n-- View: $name\n--\n\n" unless $no_comments;
350 my $text = $_->sql();
352 $output .= "$text\nGO\n";
355 # Text of procedure already has the 'create procedure' stuff
356 # so there is no need to do anything fancy. However, we should
357 # think about doing fancy stuff with granting permissions and
359 foreach ( $schema->get_procedures ) {
360 my $name = $_->name();
362 $output .= "--\n-- Procedure: $name\n--\n\n" unless $no_comments;
363 my $text = $_->sql();
365 $output .= "$text\nGO\n";
372 # -------------------------------------------------------------------
374 my ($name, $scope, $critical) = @_;
376 $scope ||= \%global_names;
377 if ( my $prev = $scope->{ $name } ) {
378 my $name_orig = $name;
379 $name .= sprintf( "%02d", ++$prev );
380 substr($name, $max_id_length - 3) = "00"
381 if length( $name ) > $max_id_length;
383 warn "The name '$name_orig' has been changed to ",
384 "'$name' to make it unique.\n" if $WARN;
386 $scope->{ $name_orig }++;
388 $name = substr( $name, 0, $max_id_length )
389 if ((length( $name ) > $max_id_length) && $critical);
394 # -------------------------------------------------------------------
396 my $name = shift || '';
397 my $schema_obj_name = shift || '';
398 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
400 # also trap fields that don't begin with a letter
401 return $name if !$reserved{ uc $name } && $name =~ /^[a-z]/i;
403 if ( $schema_obj_name ) {
404 ++$unreserve{"$schema_obj_name.$name"};
407 ++$unreserve{"$name (table name)"};
410 my $unreserve = sprintf '%s_', $name;
411 return $unreserve.$suffix;
416 # -------------------------------------------------------------------
426 Mark Addison E<lt>grommit@users.sourceforge.netE<gt> - Bulk of code from
427 Sybase producer, I just tweaked it for SQLServer. Thanks.