Release commit for 1.62
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Parser / PostgreSQL.pm
index 841ed6e..37f6579 100644 (file)
@@ -1,23 +1,5 @@
 package SQL::Translator::Parser::PostgreSQL;
 
-# -------------------------------------------------------------------
-# Copyright (C) 2002-2009 SQLFairy Authors
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; version 2.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# 02111-1307  USA
-# -------------------------------------------------------------------
-
 =head1 NAME
 
 SQL::Translator::Parser::PostgreSQL - parser for PostgreSQL
@@ -105,25 +87,22 @@ View table:
 =cut
 
 use strict;
-use vars qw[ $DEBUG $VERSION $GRAMMAR @EXPORT_OK ];
-$VERSION = '1.59';
+use warnings;
+
+our $VERSION = '1.62';
+
+our $DEBUG;
 $DEBUG   = 0 unless defined $DEBUG;
 
 use Data::Dumper;
-use Parse::RecDescent;
-use Exporter;
-use base qw(Exporter);
-
-@EXPORT_OK = qw(parse);
+use SQL::Translator::Utils qw/ddl_parser_instance/;
 
-# Enable warnings within the Parse::RecDescent module.
-$::RD_ERRORS = 1; # Make sure the parser dies when it encounters an error
-$::RD_WARN   = 1; # Enable warnings. This will warn on unused rules &c.
-$::RD_HINT   = 1; # Give out hints to help fix problems.
+use base qw(Exporter);
+our @EXPORT_OK = qw(parse);
 
-$GRAMMAR = q!
+our $GRAMMAR = <<'END_OF_GRAMMAR';
 
-{ my ( %tables, @views, $table_order, $field_order, @table_comments) }
+{ my ( %tables, @views, @triggers, $table_order, $field_order, @table_comments) }
 
 #
 # The "eofile" rule makes the parser fail if any "statement" rule
@@ -131,11 +110,16 @@ $GRAMMAR = q!
 # won't cause the failure needed to know that the parse, as a whole,
 # failed. -ky
 #
-startrule : statement(s) eofile { { tables => \%tables, views => \@views } }
+startrule : statement(s) eofile {
+    {
+        tables => \%tables,
+        views => \@views,
+        triggers => \@triggers,
+    }
+}
 
 eofile : /^\Z/
 
-
 statement : create
   | comment_on_table
   | comment_on_column
@@ -157,11 +141,11 @@ statement : create
 
 commit : /commit/i ';'
 
-connect : /^\s*\\\connect.*\n/
+connect : /^\s*\\connect.*\n/
 
 set : /set/i /[^;]*/ ';'
 
-revoke : /revoke/i WORD(s /,/) /on/i TABLE(?) table_id /from/i name_with_opt_quotes(s /,/) ';'
+revoke : /revoke/i WORD(s /,/) /on/i TABLE(?) table_id /from/i NAME(s /,/) ';'
     {
         my $table_info  = $item{'table_id'};
         my $schema_name = $table_info->{'schema_name'};
@@ -173,10 +157,10 @@ revoke : /revoke/i WORD(s /,/) /on/i TABLE(?) table_id /from/i name_with_opt_quo
         }
     }
 
-revoke : /revoke/i WORD(s /,/) /on/i SCHEMA(?) schema_name /from/i name_with_opt_quotes(s /,/) ';'
+revoke : /revoke/i WORD(s /,/) /on/i SCHEMA(?) schema_name /from/i NAME(s /,/) ';'
     { 1 }
 
-grant : /grant/i WORD(s /,/) /on/i TABLE(?) table_id /to/i name_with_opt_quotes(s /,/) ';'
+grant : /grant/i WORD(s /,/) /on/i TABLE(?) table_id /to/i NAME(s /,/) ';'
     {
         my $table_info  = $item{'table_id'};
         my $schema_name = $table_info->{'schema_name'};
@@ -188,13 +172,13 @@ grant : /grant/i WORD(s /,/) /on/i TABLE(?) table_id /to/i name_with_opt_quotes(
         }
     }
 
-grant : /grant/i WORD(s /,/) /on/i SCHEMA(?) schema_name /to/i name_with_opt_quotes(s /,/) ';'
+grant : /grant/i WORD(s /,/) /on/i SCHEMA(?) schema_name /to/i NAME(s /,/) ';'
     { 1 }
 
 drop : /drop/i /[^;]*/ ';'
 
 string :
-   /'(\\.|''|[^\\\'])*'/
+   /'(\.|''|[^\\'])*'/
 
 nonstring : /[^;\'"]+/
 
@@ -263,7 +247,8 @@ create : CREATE unique(?) /(index|key)/i index_name /on/i table_id using_method(
                 supertype => $item{'unique'}[0] ? 'constraint' : 'index',
                 type      => $item{'unique'}[0] ? 'unique'     : 'normal',
                 fields    => $item[9],
-                method    => $item{'using_method'}[0],
+                method    => $item{'using_method(?)'}[0],
+                where     => $item{'where_predicate(?)'}[0],
             }
         ;
     }
@@ -279,6 +264,34 @@ create : CREATE or_replace(?) temporary(?) VIEW view_id view_fields(?) /AS/i vie
         }
     }
 
+trigger_name : NAME
+
+trigger_scope : /FOR/i /EACH/i /(ROW|STATEMENT)/i { $return = lc $1 }
+
+before_or_after : /(before|after)/i { $return = lc $1 }
+
+trigger_action : /.+/
+
+database_event : /insert|update|delete/i
+database_events : database_event(s /OR/)
+
+create : CREATE /TRIGGER/i trigger_name before_or_after database_events /ON/i table_id trigger_scope(?) trigger_action
+    {
+        # Hack to pass roundtrip tests which have trigger statements terminated by double semicolon
+        # and expect the returned data to have the same
+        my $action = $item{trigger_action};
+        $action =~ s/;$//;
+
+        push @triggers, {
+            name => $item{trigger_name},
+            perform_action_when => $item{before_or_after},
+            database_events => $item{database_events},
+            on_table => $item{table_id}{table_name},
+            scope => $item{'trigger_scope(?)'}[0],
+            action => $action,
+        }
+    }
+
 #
 # Create anything else (e.g., domain, etc.)
 #
@@ -352,22 +365,7 @@ column_name : NAME '.' NAME
 
 comment_phrase : /null/i
     { $return = 'NULL' }
-
-comment_phrase : /'/ comment_phrase_unquoted(s) /'/
-    { my $phrase = join(' ', @{ $item[2] });
-      $return = $phrase}
-
-# [cjm TODO: double-single quotes in a comment_phrase]
-comment_phrase_unquoted : /[^\']*/
-    { $return = $item[1] }
-
-
-xxxcomment_phrase : /'.*?'|NULL/
-    {
-        my $val = $item[1] || '';
-        $val =~ s/^'|'$//g;
-        $return = $val;
-    }
+    | SQSTRING
 
 field : field_comment(s?) field_name data_type field_meta(s?) field_comment(s?)
     {
@@ -441,7 +439,7 @@ column_constraint : constraint_name(?) column_constraint_type deferrable(?) defe
         }
     }
 
-constraint_name : /constraint/i name_with_opt_quotes { $item[2] }
+constraint_name : /constraint/i NAME { $item[2] }
 
 column_constraint_type : /not null/i { $return = { type => 'not_null' } }
     |
@@ -478,11 +476,11 @@ column_constraint_type : /not null/i { $return = { type => 'not_null' } }
         }
     }
 
-table_id : schema_qualification(?) name_with_opt_quotes {
+table_id : schema_qualification(?) NAME {
     $return = { schema_name => $item[1][0], table_name => $item[2] }
 }
 
-view_id : schema_qualification(?) name_with_opt_quotes {
+view_id : schema_qualification(?) NAME {
     $return = { schema_name => $item[1][0], view_name => $item[2] }
 }
 
@@ -498,27 +496,30 @@ view_target : '('   /select/i    / [^;]+ (?= \) ) /x    ')'    {
 
 view_target_spec :
 
-schema_qualification : name_with_opt_quotes '.'
+schema_qualification : NAME '.'
 
-schema_name : name_with_opt_quotes
+schema_name : NAME
 
-field_name : name_with_opt_quotes
-
-name_with_opt_quotes : double_quote(?) NAME double_quote(?) { $item[2] }
+field_name : NAME
 
 double_quote: /"/
 
-index_name : name_with_opt_quotes
+index_name : NAME
+
+array_indicator : '[' ']'
+    { $return = $item[1].$item[2] }
 
-data_type : pg_data_type parens_value_list(?)
+data_type : pg_data_type parens_value_list(?) array_indicator(?)
     {
         my $data_type = $item[1];
 
+        $data_type->{type} .= $item[3][0] if $item[3][0];
+
         #
         # We can deduce some sizes from the data type's name.
         #
-        if ( my $size = $item[2][0] ) {
-            $data_type->{'size'} = $size;
+        if ( my @size = @{$item[2]} ) {
+            $data_type->{'size'} = (@size == 1 ? $size[0] : \@size);
         }
 
         $return  = $data_type;
@@ -613,11 +614,16 @@ pg_data_type :
             $return = { type => 'bytea' };
         }
     |
-    /(timestamptz|timestamp)(?:\(\d\))?( with(?:out)? time zone)?/i
+    / ( timestamp (?:tz)? ) (?: \( \d \) )? ( \s with (?:out)? \s time \s zone )? /ix
         {
             $return = { type => 'timestamp' . ($2||'') };
         }
     |
+    / ( time (?:tz)? ) (?: \( \d \) )? ( \s with (?:out)? \s time \s zone )? /ix
+        {
+            $return = { type => 'time' . ($2||'') };
+        }
+    |
     /text/i
         {
             $return = {
@@ -626,7 +632,7 @@ pg_data_type :
             };
         }
     |
-    /(bit|box|cidr|circle|date|inet|line|lseg|macaddr|money|numeric|decimal|path|point|polygon|timetz|time|varchar)/i
+    /(bit|box|cidr|circle|date|inet|line|lseg|macaddr|money|numeric|decimal|path|point|polygon|varchar|json|hstore|uuid)/i
         {
             $return = { type => $item[1] };
         }
@@ -635,7 +641,7 @@ parens_value_list : '(' VALUE(s /,/) ')'
     { $item[2] }
 
 
-parens_word_list : '(' name_with_opt_quotes(s /,/) ')'
+parens_word_list : '(' NAME(s /,/) ')'
     { $item[2] }
 
 field_size : '(' num_range ')' { $item{'num_range'} }
@@ -670,7 +676,7 @@ table_constraint : comment(s?) constraint_name(?) table_constraint_type deferrab
         }
     }
 
-table_constraint_type : /primary key/i '(' name_with_opt_quotes(s /,/) ')'
+table_constraint_type : /primary key/i '(' NAME(s /,/) ')'
     {
         $return = {
             type   => 'primary_key',
@@ -678,7 +684,7 @@ table_constraint_type : /primary key/i '(' name_with_opt_quotes(s /,/) ')'
         }
     }
     |
-    /unique/i '(' name_with_opt_quotes(s /,/) ')'
+    /unique/i '(' NAME(s /,/) ')'
     {
         $return    =  {
             type   => 'unique',
@@ -694,7 +700,7 @@ table_constraint_type : /primary key/i '(' name_with_opt_quotes(s /,/) ')'
         }
     }
     |
-    /foreign key/i '(' name_with_opt_quotes(s /,/) ')' /references/i table_id parens_word_list(?) match_type(?) key_action(s?)
+    /foreign key/i '(' NAME(s /,/) ')' /references/i table_id parens_word_list(?) match_type(?) key_action(s?)
     {
         my ( $on_delete, $on_update );
         for my $action ( @{ $item[9] || [] } ) {
@@ -809,7 +815,7 @@ alter : alter_sequence NAME /owned/i /by/i column_name ';'
 
 storage_type : /(plain|external|extended|main)/i
 
-temporary : /temp(orary)?\\b/i
+temporary : /temp(orary)?\b/i
   {
     1;
   }
@@ -918,10 +924,10 @@ create_table : CREATE TABLE
 
 create_index : CREATE /index/i
 
-default_val  : DEFAULT /(\d+|'[^']*'|\w+\(.*\))|\w+/
+default_val  : DEFAULT DEFAULT_VALUE ( '::' data_type )(?)
     {
-        my $val =  defined $item[2] ? $item[2] : '';
-        $val    =~ s/^'|'$//g;
+        my $val =  $item[2];
+        $val =~ s/^\((\d+)\)\z/$1/; # for example (0)::smallint
         $return =  {
             supertype => 'constraint',
             type      => 'default',
@@ -937,6 +943,11 @@ default_val  : DEFAULT /(\d+|'[^']*'|\w+\(.*\))|\w+/
         }
     }
 
+DEFAULT_VALUE : VALUE
+    | /\w+\(.*\)/
+    | /\w+/
+    | /\(\d+\)/
+
 name_with_opt_paren : NAME parens_value_list(s?)
     { $item[2][0] ? "$item[1]($item[2][0][0])" : $item[1] }
 
@@ -944,7 +955,7 @@ unique : /unique/i { 1 }
 
 key : /key/i | /index/i
 
-table_option : /inherits/i '(' name_with_opt_quotes(s /,/) ')'
+table_option : /inherits/i '(' NAME(s /,/) ')'
     {
         $return = { type => 'inherits', table_name => $item[3] }
     }
@@ -992,34 +1003,38 @@ COMMA : ','
 
 SET : /set/i
 
-NAME    : "`" /\w+/ "`"
-    { $item[2] }
+NAME : DQSTRING
     | /\w+/
-    { $item[1] }
-    | /[\$\w]+/
-    { $item[1] }
-
-VALUE   : /[-+]?\.?\d+(?:[eE]\d+)?/
-    { $item[1] }
-    | /'.*?'/   # XXX doesn't handle embedded quotes
-    { $item[1] }
+
+DQSTRING : '"' <skip: ''> /((?:[^"]|"")+)/ '"'
+    { ($return = $item[3]) =~ s/""/"/g; }
+
+SQSTRING : "'" <skip: ''> /((?:[^']|'')*)/ "'"
+    { ($return = $item[3]) =~ s/''/'/g }
+
+DOLLARSTRING : /\$[^\$]*\$/ <skip: ''> /.*?(?=\Q$item[1]\E)/s "$item[1]"
+    { $return = $item[3]; }
+
+VALUE : /[-+]?\d*\.?\d+(?:[eE]\d+)?/
+    | SQSTRING
+    | DOLLARSTRING
     | /null/i
     { 'NULL' }
 
-!;
+END_OF_GRAMMAR
 
-# -------------------------------------------------------------------
 sub parse {
     my ( $translator, $data ) = @_;
-    my $parser = Parse::RecDescent->new($GRAMMAR);
 
-    $::RD_TRACE  = $translator->trace ? 1 : undef;
-    $DEBUG       = $translator->debug;
+    # Enable warnings within the Parse::RecDescent module.
+    local $::RD_ERRORS = 1 unless defined $::RD_ERRORS; # Make sure the parser dies when it encounters an error
+    local $::RD_WARN   = 1 unless defined $::RD_WARN; # Enable warnings. This will warn on unused rules &c.
+    local $::RD_HINT   = 1 unless defined $::RD_HINT; # Give out hints to help fix problems.
 
-    unless (defined $parser) {
-        return $translator->error("Error instantiating Parse::RecDescent ".
-            "instance: Bad grammer");
-    }
+    local $::RD_TRACE  = $translator->trace ? 1 : undef;
+    local $DEBUG       = $translator->debug;
+
+    my $parser = ddl_parser_instance('PostgreSQL');
 
     my $result = $parser->startrule($data);
     die "Parse failed.\n" unless defined $result;
@@ -1070,10 +1085,14 @@ sub parse {
         }
 
         for my $idata ( @{ $tdata->{'indices'} || [] } ) {
+            my @options = ();
+            push @options, { using => $idata->{'method'} } if $idata->{method};
+            push @options, { where => $idata->{'where'} }  if $idata->{where};
             my $index  =  $table->add_index(
-                name   => $idata->{'name'},
-                type   => uc $idata->{'type'},
-                fields => $idata->{'fields'},
+                name    => $idata->{'name'},
+                type    => uc $idata->{'type'},
+                fields  => $idata->{'fields'},
+                options => \@options
             ) or die $table->error . ' ' . $table->name;
         }
 
@@ -1106,6 +1125,10 @@ sub parse {
       $view->extra ( temporary => 1 ) if $vinfo->{is_temporary};
     }
 
+    for my $trigger (@{ $result->{triggers} }) {
+        $schema->add_trigger( %$trigger );
+    }
+
     return 1;
 }