Escape quotes in string values in producers
Dagfinn Ilmari Mannsåker [Tue, 1 Jul 2014 08:19:59 +0000 (09:19 +0100)]
12 files changed:
lib/SQL/Translator/Generator/DDL/SQLServer.pm
lib/SQL/Translator/Generator/Role/DDL.pm
lib/SQL/Translator/Generator/Role/Quote.pm
lib/SQL/Translator/Producer.pm
lib/SQL/Translator/Producer/MySQL.pm
lib/SQL/Translator/Producer/Oracle.pm
lib/SQL/Translator/Producer/PostgreSQL.pm
t/02mysql-parser.t
t/03mysql-to-oracle.t
t/38-mysql-producer.t
t/47postgres-producer.t
t/71-generator-sql_server.t

index 9e35d89..7221229 100644 (file)
@@ -136,7 +136,7 @@ sub enum_constraint {
   return (
      'CONSTRAINT ' . $self->enum_constraint_name($field_name) .
        ' CHECK (' . $self->quote($field_name) .
-       ' IN (' . join( ',', map qq('$_'), @$vals ) . '))'
+       ' IN (' . join( ',', map $self->quote_string($_), @$vals ) . '))'
   )
 }
 
index e7666c7..83c8647 100644 (file)
@@ -20,6 +20,7 @@ requires '_build_numeric_types';
 requires '_build_unquoted_defaults';
 requires '_build_sizeless_types';
 requires 'quote';
+requires 'quote_string';
 
 has type_map => (
    is => 'lazy',
@@ -81,7 +82,7 @@ sub field_default {
   if (ref $default) {
       $default = $$default;
   } elsif (!($self->numeric_types->{lc($field->data_type)} && Scalar::Util::looks_like_number ($default))) {
-     $default = "'$default'";
+      $default = $self->quote_string($default);
   }
   return ( "DEFAULT $default" )
 }
index 33725c3..43c54b3 100644 (file)
@@ -47,6 +47,14 @@ sub quote {
   join $sep, map { (my $n = $_) =~ s/\Q$r/$esc$r/g; "$l$n$r" } ( $sep ? split (/\Q$sep\E/, $label ) : $label )
 }
 
+sub quote_string {
+    my ($self, $string) = @_;
+
+    return $string unless defined $string;
+    $string =~ s/'/''/g;
+    return qq{'$string'};
+}
+
 1;
 
 =head1 AUTHORS
index ac6d5bd..94a3d43 100644 (file)
@@ -15,7 +15,7 @@ sub produce { "" }
 ## They are special per Producer, and provide support for the old 'now()'
 ## default value exceptions
 sub _apply_default_value {
-  my (undef, $field, $field_ref, $exceptions) = @_;
+  my ($self, $field, $field_ref, $exceptions) = @_;
   my $default = $field->default_value;
   return if !defined $default;
 
@@ -41,11 +41,18 @@ sub _apply_default_value {
     # we need to check the data itself in addition to the datatype, for basic safety
       $$field_ref .= " DEFAULT $default";
   } else {
-      $$field_ref .= " DEFAULT '$default'";
+      $default = $self->_quote_string($default);
+      $$field_ref .= " DEFAULT $default";
   }
 
 }
 
+sub _quote_string {
+    my ($self, $string) = @_;
+    $string =~ s/'/''/g;
+    return qq{'$string'};
+}
+
 1;
 
 # -------------------------------------------------------------------
index 57d09dc..904b294 100644 (file)
@@ -90,6 +90,7 @@ $DEBUG   = 0 unless defined $DEBUG;
 #   http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
 my $DEFAULT_MAX_ID_LENGTH = 64;
 
+use base qw(SQL::Translator::Producer);
 use Data::Dumper;
 use SQL::Translator::Schema::Constants;
 use SQL::Translator::Generator::DDL::MySQL;
@@ -534,8 +535,7 @@ sub create_field
     my @size      = $field->size;
     my %extra     = $field->extra;
     my $list      = $extra{'list'} || [];
-    # \todo deal with embedded quotes
-    my $commalist = join( ', ', map { qq['$_'] } @$list );
+    my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
     my $charset = $extra{'mysql_charset'};
     my $collate = $extra{'mysql_collate'};
 
@@ -627,7 +627,7 @@ sub create_field
     }
 
     # Default?
-    SQL::Translator::Producer->_apply_default_value(
+    __PACKAGE__->_apply_default_value(
       $field,
       \$field_def,
       [
@@ -636,7 +636,8 @@ sub create_field
     );
 
     if ( my $comments = $field->comments ) {
-        $field_def .= qq[ comment '$comments'];
+        $comments = __PACKAGE__->_quote_string($comments);
+        $field_def .= qq[ comment $comments];
     }
 
     # auto_increment?
@@ -645,6 +646,13 @@ sub create_field
     return $field_def;
 }
 
+sub _quote_string {
+    my ($self, $string) = @_;
+
+    $string =~ s/([\\'])/$1$1/g;
+    return qq{'$string'};
+}
+
 sub alter_create_index
 {
     my ($index, $options) = @_;
index 34a9be8..d3f7a12 100644 (file)
@@ -93,6 +93,7 @@ our ( $DEBUG, $WARN );
 our $VERSION = '1.59';
 $DEBUG   = 0 unless defined $DEBUG;
 
+use base 'SQL::Translator::Producer';
 use SQL::Translator::Schema::Constants;
 use SQL::Translator::Utils qw(header_comment);
 
@@ -461,10 +462,9 @@ sub create_table {
         if ( my @table_comments = $table->comments ) {
             for my $comment ( @table_comments ) {
                 next unless $comment;
-                $comment =~ s/'/''/g;
-                push @field_comments, "COMMENT ON TABLE $table_name_q is\n '".
-                $comment . "'" unless $options->{no_comments}
-                ;
+                $comment = __PACKAGE__->_quote_string($comment);
+                push @field_comments, "COMMENT ON TABLE $table_name_q is\n $comment"
+                    unless $options->{no_comments};
             }
         }
 
@@ -550,8 +550,7 @@ sub create_field {
     my @size      = $field->size;
     my %extra     = $field->extra;
     my $list      = $extra{'list'} || [];
-    # \todo deal with embedded quotes
-    my $commalist = join( ', ', map { qq['$_'] } @$list );
+    my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
 
     if ( $data_type eq 'enum' ) {
         $check = "CHECK ($field_name_q IN ($commalist))";
@@ -660,7 +659,7 @@ sub create_field {
                 ) {
             $default = 'SYSDATE';
         } else {
-            $default = $default =~ m/null/i ? 'NULL' : "'$default'"
+            $default = $default =~ m/null/i ? 'NULL' : __PACKAGE__->_quote_string($default);
         }
 
         $field_def .= " DEFAULT $default",
@@ -718,10 +717,10 @@ sub create_field {
     push @field_defs, $field_def;
 
     if ( my $comment = $field->comments ) {
-        $comment =~ s/'/''/g;
+        $comment =~ __PACKAGE__->_quote_string($comment);
         push @field_comments,
-          "COMMENT ON COLUMN $table_name_q.$field_name_q is\n '" .
-            $comment . "';" unless $options->{no_comments};
+          "COMMENT ON COLUMN $table_name_q.$field_name_q is\n $comment;"
+              unless $options->{no_comments};
     }
 
     return \@create, \@field_defs, \@trigger_defs, \@field_comments;
index fa618a3..4fffce3 100644 (file)
@@ -460,8 +460,7 @@ sub create_view {
         my $data_type = lc $field->data_type;
         my %extra     = $field->extra;
         my $list      = $extra{'list'} || [];
-        # todo deal with embedded quotes
-        my $commalist = join( ', ', map { qq['$_'] } @$list );
+        my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
 
         if ($postgres_version >= 8.003 && $field->data_type eq 'enum') {
             my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
@@ -480,7 +479,7 @@ sub create_view {
         #
         # Default value
         #
-        SQL::Translator::Producer->_apply_default_value(
+        __PACKAGE__->_apply_default_value(
           $field,
           \$field_def,
           [
@@ -798,8 +797,7 @@ sub alter_field
     if(ref $default_value eq "SCALAR" ) {
         $default_value = $$default_value;
     } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
-        $default_value =~ s/'/''/xsmg;
-        $default_value = q(') . $default_value . q(');
+        $default_value = __PACKAGE__->_quote_string($default_value);
     }
 
     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
index 63ac5f3..1bc8888 100644 (file)
@@ -80,7 +80,7 @@ BEGIN {
               s1 set('a','b','c') default 'b',
               e1 enum("a","b","c") default "c",
               name varchar(30) default NULL,
-              foo_type enum('vk','ck') NOT NULL default 'vk',
+              foo_type enum('vk','c''k') NOT NULL default 'vk',
               date timestamp,
               time_stamp2 timestamp,
               foo_enabled bit(1) default b'0',
@@ -170,12 +170,12 @@ BEGIN {
     my $f8 = shift @fields;
     is( $f8->name, 'foo_type', 'Eighth field name is "foo_type"' );
     is( $f8->data_type, 'enum', 'Type is "enum"' );
-    is( $f8->size, 2, 'Size is "2"' );
+    is( $f8->size, 3, 'Size is "2"' );
     is( $f8->is_nullable, 0, 'Field cannot be null' );
     is( $f8->default_value, 'vk', 'Default value is "vk"' );
     is( $f8->is_primary_key, 0, 'Field is not PK' );
     my %f8extra = $f8->extra;
-    is( join(',', @{ $f8extra{'list'} || [] }), 'vk,ck', 'List is "vk,ck"' );
+    is( join(',', @{ $f8extra{'list'} || [] }), 'vk,c\'k', 'List is "vk,c\'k"' );
 
     my $f9 = shift @fields;
     is( $f9->name, 'date', 'Ninth field name is "date"' );
index 664d4e9..5b64c59 100644 (file)
@@ -10,6 +10,7 @@ my $create = q|
 CREATE TABLE random (
     id int auto_increment PRIMARY KEY,
     foo varchar(255) not null default '',
+    bar enum('wibble','wo''bble'),
     updated timestamp
 );
 CREATE UNIQUE INDEX random_foo_update ON random(foo,updated);
@@ -18,7 +19,7 @@ CREATE INDEX random_foo ON random(foo);
 |;
 
 BEGIN {
-    maybe_plan(3,
+    maybe_plan(undef,
         'SQL::Translator::Parser::MySQL',
         'SQL::Translator::Producer::Oracle');
 }
@@ -35,3 +36,6 @@ my $output = $tr->translate(\$create);
 ok( $output, 'Translate MySQL to Oracle' );
 ok( $output =~ /CREATE INDEX random_foo /, 'Normal index definition translated.');
 ok( $output =~ /CREATE UNIQUE INDEX random_foo_update /, 'Unique index definition translated.');
+ok( $output =~ /\QCHECK (bar IN ('wibble', 'wo''bble'))\E/, 'Enum translated and escaped.');
+
+done_testing;
index 0e3f9c6..446186b 100644 (file)
@@ -117,7 +117,7 @@ schema:
             list:
               - foo
               - bar
-              - baz
+              - ba'z
       indices:
         - type: NORMAL
           fields:
@@ -171,7 +171,7 @@ schema:
             list:
               - foo
               - bar
-              - baz
+              - ba'z
       indices:
         - type: NORMAL
           fields:
@@ -215,7 +215,7 @@ my @stmts = (
   `id` integer NOT NULL,
   `foo` integer NOT NULL,
   `foo2` integer NULL,
-  `bar_set` set('foo', 'bar', 'baz') NULL,
+  `bar_set` set('foo', 'bar', 'ba''z') NULL,
   INDEX `index_1` (`id`),
   INDEX `really_long_name_bigger_than_64_chars_aaaaaaaaaaaaaaaaa_aed44c47` (`id`),
   INDEX (`foo`),
@@ -230,7 +230,7 @@ my @stmts = (
   `id` integer NOT NULL,
   `foo` integer NOT NULL,
   `foo2` integer NULL,
-  `bar_set` set('foo', 'bar', 'baz') NULL,
+  `bar_set` set('foo', 'bar', 'ba''z') NULL,
   INDEX `index_1` (`id`),
   INDEX `really_long_name_bigger_than_64_chars_aaaaaaaaaaaaaaaaa_aed44c47` (`id`),
   INDEX (`foo`),
index 8297b81..3e0ac90 100644 (file)
@@ -14,7 +14,7 @@ use FindBin qw/$Bin/;
 #=============================================================================
 
 BEGIN {
-    maybe_plan(57,
+    maybe_plan(undef,
         'SQL::Translator::Producer::PostgreSQL',
         'Test::Differences',
     )
@@ -306,18 +306,30 @@ is($field4_sql, 'bytea_field bytea NOT NULL', 'Create bytea field works');
 my $field5 = SQL::Translator::Schema::Field->new( name => 'enum_field',
                                                    table => $table,
                                                    data_type => 'enum',
-                                                   extra => { list => [ 'Foo', 'Bar' ] },
+                                                   extra => { list => [ 'Foo', 'Bar', 'Ba\'z' ] },
                                                    is_auto_increment => 0,
                                                    is_nullable => 0,
                                                    is_foreign_key => 0,
                                                    is_unique => 0 );
 
-my $field5_sql = SQL::Translator::Producer::PostgreSQL::create_field($field5,{ postgres_version => 8.3 });
+my $field5_types = {};
+my $field5_sql = SQL::Translator::Producer::PostgreSQL::create_field(
+    $field5,
+    {
+        postgres_version => 8.3,
+        type_defs => $field5_types,
+    }
+);
 
 is($field5_sql, 'enum_field mytable_enum_field_type NOT NULL', 'Create real enum field works');
-
-
-
+is_deeply(
+    $field5_types,
+    { mytable_enum_field_type =>
+          "DROP TYPE IF EXISTS mytable_enum_field_type CASCADE;\n" .
+          "CREATE TYPE mytable_enum_field_type AS ENUM ('Foo', 'Bar', 'Ba''z')"
+    },
+    'Create real enum type works'
+);
 
 my $field6 = SQL::Translator::Schema::Field->new(
                                                   name      => 'character',
@@ -438,16 +450,31 @@ is($field12_sql, 'time_field timestamp NOT NULL', 'time with precision');
 my $field13 = SQL::Translator::Schema::Field->new( name => 'enum_field_with_type_name',
                                                    table => $table,
                                                    data_type => 'enum',
-                                                   extra => { list => [ 'Foo', 'Bar' ],
+                                                   extra => { list => [ 'Foo', 'Bar', 'Ba\'z' ],
                                                               custom_type_name => 'real_enum_type' },
                                                    is_auto_increment => 0,
                                                    is_nullable => 0,
                                                    is_foreign_key => 0,
                                                    is_unique => 0 );
 
-my $field13_sql = SQL::Translator::Producer::PostgreSQL::create_field($field13,{ postgres_version => 8.3 });
+my $field13_types = {};
+my $field13_sql = SQL::Translator::Producer::PostgreSQL::create_field(
+    $field13,
+    {
+        postgres_version => 8.3,
+        type_defs => $field13_types,
+    }
+);
 
 is($field13_sql, 'enum_field_with_type_name real_enum_type NOT NULL', 'Create real enum field works');
+is_deeply(
+    $field13_types,
+    { real_enum_type =>
+          "DROP TYPE IF EXISTS real_enum_type CASCADE;\n" .
+          "CREATE TYPE real_enum_type AS ENUM ('Foo', 'Bar', 'Ba''z')"
+    },
+    'Create real enum type works'
+);
 
 
 {
@@ -621,3 +648,5 @@ CREATE VIEW view_foo ( id, name ) AS
 ";
 
 is($drop_view_9_1_produced, $drop_view_9_1_expected, "My DROP VIEW statement for 9.1 is correct");
+
+done_testing;
index 17b0da3..9e31ce4 100644 (file)
@@ -5,6 +5,7 @@ use Test::More;
 
 use SQL::Translator::Generator::DDL::SQLServer;
 use SQL::Translator::Schema::Field;
+use SQL::Translator::Schema::Table;
 
 my $shim = SQL::Translator::Generator::DDL::SQLServer->new();
 
@@ -19,5 +20,19 @@ is $shim->field(SQL::Translator::Schema::Field->new(
    size => 10,
 )), '[nice] varchar(10) NULL', 'sized field is generated correctly';
 
+my $table = SQL::Translator::Schema::Table->new(
+    name => 'mytable',
+);
+
+$table->add_field(
+    name => 'myenum',
+    data_type => 'enum',
+    extra => { list => [qw(foo ba'r)] },
+);
+
+like $shim->table($table),
+     qr/\b\QCONSTRAINT [myenum_chk] CHECK ([myenum] IN ('foo','ba''r'))\E/,
+     'enum constraint is generated and escaped correctly';
+
 done_testing;