File Coverage

blib/lib/SQL/Translator/Producer/PostgreSQL.pm
Criterion Covered Total %
statement 414 459 90.2
branch 183 258 70.9
condition 94 130 72.3
subroutine 43 46 93.4
pod 10 28 35.7
total 744 921 80.7


line stmt bran cond sub pod time code
1             package SQL::Translator::Producer::PostgreSQL;
2              
3             =head1 NAME
4              
5             SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
6              
7             =head1 SYNOPSIS
8              
9             my $t = SQL::Translator->new( parser => '...', producer => 'PostgreSQL' );
10             $t->translate;
11              
12             =head1 DESCRIPTION
13              
14             Creates a DDL suitable for PostgreSQL. Very heavily based on the Oracle
15             producer.
16              
17             Now handles PostGIS Geometry and Geography data types on table definitions.
18             Does not yet support PostGIS Views.
19              
20             =head2 Producer Args
21              
22             You can change the global behavior of the producer by passing the following options to the
23             C attribute of C.
24              
25             =over 4
26              
27             =item postgres_version
28              
29             The version of postgres to generate DDL for. Turns on features only available in later versions. The following features are supported
30              
31             =over 4
32              
33             =item IF EXISTS
34              
35             If your postgres_version is higher than 8.003 (I should hope it is by now), then the DDL
36             generated for dropping objects in the database will contain IF EXISTS.
37              
38             =back
39              
40             =item attach_comments
41              
42             Generates table and column comments via the COMMENT command rather than as a comment in
43             the DDL. You could then look it up with \dt+ or \d+ (for tables and columns respectively)
44             in psql. The comment is dollar quoted with $comment$ so you can include ' in it. Just to clarify: you get this
45              
46             CREATE TABLE foo ...;
47             COMMENT on TABLE foo IS $comment$hi there$comment$;
48              
49             instead of this
50              
51             -- comment
52             CREAT TABLE foo ...;
53              
54             =back
55              
56             =head2 Extra args
57              
58             Various schema types support various options via the C attribute.
59              
60             =over 2
61              
62             =item Tables
63              
64             =over 2
65              
66             =item temporary
67              
68             Produces a temporary table.
69              
70             =back
71              
72             =item Views
73              
74             =over 2
75              
76             =item temporary
77              
78             Produces a temporary view.
79              
80             =item materialized
81              
82             Produces a materialized view.
83              
84             =back
85              
86             =item Fields
87              
88             =over 2
89              
90             =item list, custom_type_name
91              
92             For enum types, list is the list of valid values, and custom_type_name is the name that
93             the type should have. Defaults to $table_$field_type.
94              
95             =item geometry_type, srid, dimensions, geography_type
96              
97             Fields for use with PostGIS types.
98              
99             =back
100              
101             =back
102              
103             =cut
104              
105 6     6   4631 use strict;
  6         587  
  6         282  
106 6     6   41 use warnings;
  6         14  
  6         757  
107             our ($DEBUG, $WARN);
108             our $VERSION = '1.66';
109             $DEBUG = 0 unless defined $DEBUG;
110              
111 6     6   44 use base qw(SQL::Translator::Producer);
  6         12  
  6         1660  
112 6     6   45 use SQL::Translator::Schema::Constants;
  6         13  
  6         700  
113             use SQL::Translator::Utils
114 6     6   1049 qw(debug header_comment parse_dbms_version batch_alter_table_statements normalize_quote_options);
  6         17  
  6         614  
115 6     6   4408 use SQL::Translator::Generator::DDL::PostgreSQL;
  6         32  
  6         283  
116 6     6   52 use Data::Dumper;
  6         13  
  6         528  
117              
118 6     6   73 use constant MAX_ID_LENGTH => 62;
  6         15  
  6         1916  
119              
120             {
121             my ($quoting_generator, $nonquoting_generator);
122              
123             sub _generator {
124 325     325   778 my $options = shift;
125 325 100       1372 return $options->{generator} if exists $options->{generator};
126              
127 166 100 66     536 return normalize_quote_options($options)
      66        
128             ? $quoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new
129             : $nonquoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new(quote_chars => [],);
130             }
131             }
132              
133             my (%translate);
134              
135             BEGIN {
136              
137 6     6   58407 %translate = (
138             #
139             # MySQL types
140             #
141             double => 'double precision',
142             decimal => 'numeric',
143             int => 'integer',
144             mediumint => 'integer',
145             tinyint => 'smallint',
146             char => 'character',
147             varchar => 'character varying',
148             longtext => 'text',
149             mediumtext => 'text',
150             tinytext => 'text',
151             tinyblob => 'bytea',
152             blob => 'bytea',
153             mediumblob => 'bytea',
154             longblob => 'bytea',
155             enum => 'character varying',
156             set => 'character varying',
157             datetime => 'timestamp',
158             year => 'date',
159              
160             #
161             # Oracle types
162             #
163             number => 'integer',
164             varchar2 => 'character varying',
165             long => 'text',
166             clob => 'text',
167              
168             #
169             # Sybase types
170             #
171             comment => 'text',
172              
173             #
174             # MS Access types
175             #
176             memo => 'text',
177             );
178             }
179             my %truncated;
180              
181             =pod
182              
183             =head1 PostgreSQL Create Table Syntax
184              
185             CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
186             { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
187             | table_constraint } [, ... ]
188             )
189             [ INHERITS ( parent_table [, ... ] ) ]
190             [ WITH OIDS | WITHOUT OIDS ]
191              
192             where column_constraint is:
193              
194             [ CONSTRAINT constraint_name ]
195             { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
196             CHECK (expression) |
197             REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
198             [ ON DELETE action ] [ ON UPDATE action ] }
199             [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
200              
201             and table_constraint is:
202              
203             [ CONSTRAINT constraint_name ]
204             { UNIQUE ( column_name [, ... ] ) |
205             PRIMARY KEY ( column_name [, ... ] ) |
206             CHECK ( expression ) |
207             EXCLUDE [USING acc_method] (expression) [INCLUDE (column [, ...])] [WHERE (predicate)]
208             FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
209             [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
210             [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
211              
212             =head1 Create Index Syntax
213              
214             CREATE [ UNIQUE ] INDEX index_name ON table
215             [ USING acc_method ] ( column [ ops_name ] [, ...] )
216             [ INCLUDE ( column [, ...] ) ]
217             [ WHERE predicate ]
218             CREATE [ UNIQUE ] INDEX index_name ON table
219             [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
220             [ WHERE predicate ]
221              
222             =cut
223              
224             sub produce {
225 8     8 1 23 my $translator = shift;
226 8         64 local $DEBUG = $translator->debug;
227 8         279 local $WARN = $translator->show_warnings;
228 8         286 my $no_comments = $translator->no_comments;
229 8         256 my $add_drop_table = $translator->add_drop_table;
230 8         287 my $schema = $translator->schema;
231 8         297 my $pargs = $translator->producer_args;
232 8         73 my $postgres_version = parse_dbms_version($pargs->{postgres_version}, 'perl');
233              
234 8         323 my $generator = _generator({ quote_identifiers => $translator->quote_identifiers });
235              
236 8         63 my @output;
237 8 50       51 push @output, header_comment unless ($no_comments);
238              
239 8         40 my (@table_defs, @fks);
240 8         0 my %type_defs;
241 8         64 for my $table ($schema->get_tables) {
242              
243             my ($table_def, $fks) = create_table(
244             $table,
245             {
246             generator => $generator,
247             no_comments => $no_comments,
248             postgres_version => $postgres_version,
249             add_drop_table => $add_drop_table,
250             type_defs => \%type_defs,
251             attach_comments => $pargs->{attach_comments}
252             }
253 22         351 );
254              
255 22         161 push @table_defs, $table_def;
256 22         91 push @fks, @$fks;
257             }
258              
259 8         74 for my $view ($schema->get_views) {
260 5         87 push @table_defs,
261             create_view(
262             $view,
263             {
264             postgres_version => $postgres_version,
265             add_drop_view => $add_drop_table,
266             generator => $generator,
267             no_comments => $no_comments,
268             }
269             );
270             }
271              
272 8         52 for my $trigger ($schema->get_triggers) {
273 14         142 push @table_defs,
274             create_trigger(
275             $trigger,
276             {
277             add_drop_trigger => $add_drop_table,
278             generator => $generator,
279             no_comments => $no_comments,
280             }
281             );
282             }
283              
284 8         36 push @output, map {"$_;\n\n"} values %type_defs;
  0         0  
285 8         29 push @output, map {"$_;\n\n"} @table_defs;
  55         169  
286 8 100       61 if (@fks) {
287 5 50       22 push @output, "--\n-- Foreign Key Definitions\n--\n\n"
288             unless $no_comments;
289 5         17 push @output, map {"$_;\n\n"} @fks;
  5         31  
290             }
291              
292 8 100       43 if ($WARN) {
293 1 50       4 if (%truncated) {
294 0         0 warn "Truncated " . keys(%truncated) . " names:\n";
295 0         0 warn "\t" . join("\n\t", sort keys %truncated) . "\n";
296             }
297             }
298              
299             return wantarray
300             ? @output
301 8 100       3564 : join('', @output);
302             }
303              
304             {
305             my %global_names;
306              
307             sub mk_name {
308 0   0 0 0 0 my $basename = shift || '';
309 0   0     0 my $type = shift || '';
310 0   0     0 my $scope = shift || '';
311 0   0     0 my $critical = shift || '';
312 0         0 my $basename_orig = $basename;
313              
314 0 0       0 my $max_name
315             = $type
316             ? MAX_ID_LENGTH - (length($type) + 1)
317             : MAX_ID_LENGTH;
318 0 0       0 $basename = substr($basename, 0, $max_name)
319             if length($basename) > $max_name;
320 0 0       0 my $name = $type ? "${type}_$basename" : $basename;
321              
322 0 0 0     0 if ($basename ne $basename_orig and $critical) {
323 0 0       0 my $show_type = $type ? "+'$type'" : "";
324 0 0       0 warn "Truncating '$basename_orig'$show_type to ", MAX_ID_LENGTH, " character limit to make '$name'\n"
325             if $WARN;
326 0         0 $truncated{$basename_orig} = $name;
327             }
328              
329 0   0     0 $scope ||= \%global_names;
330 0 0       0 if (my $prev = $scope->{$name}) {
331 0         0 my $name_orig = $name;
332 0         0 $name .= sprintf("%02d", ++$prev);
333 0 0       0 substr($name, MAX_ID_LENGTH - 3) = "00"
334             if length($name) > MAX_ID_LENGTH;
335              
336 0 0       0 warn "The name '$name_orig' has been changed to ", "'$name' to make it unique.\n"
337             if $WARN;
338              
339 0         0 $scope->{$name_orig}++;
340             }
341              
342 0         0 $scope->{$name}++;
343 0         0 return $name;
344             }
345             }
346              
347             sub is_geometry {
348 240     240 0 631 my $field = shift;
349 240 100       1510 return 1 if $field->data_type eq 'geometry';
350             }
351              
352             sub is_geography {
353 0     0 0 0 my $field = shift;
354 0 0       0 return 1 if $field->data_type eq 'geography';
355             }
356              
357             sub create_table {
358 24     24 1 102 my ($table, $options) = @_;
359              
360 24         102 my $generator = _generator($options);
361 24   100     153 my $no_comments = $options->{no_comments} || 0;
362 24   100     117 my $add_drop_table = $options->{add_drop_table} || 0;
363 24   50     157 my $postgres_version = $options->{postgres_version} || 0;
364 24   100     124 my $type_defs = $options->{type_defs} || {};
365 24         80 my $attach_comments = $options->{attach_comments};
366              
367 24 50       867 my $table_name = $table->name or next;
368 24         980 my $table_name_qt = $generator->quote($table_name);
369              
370 24         87 my (@comments, @field_defs, @index_defs, @constraint_defs, @fks);
371              
372 24 100       153 push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
373              
374 24         54 my @comment_statements;
375 24 100       742 if (my $comments = $table->comments) {
376 1 50       23 if ($attach_comments) {
    0          
377              
378             # this follows the example in the MySQL producer, where all comments are added as
379             # table comments, even though they could have originally been parsed as DDL comments
380             # quoted via $$ string so there can be 'quotes' inside the comments
381 1         3 my $comment_ddl = "COMMENT on TABLE $table_name_qt IS \$comment\$$comments\$comment\$";
382 1         4 push @comment_statements, $comment_ddl;
383             } elsif (!$no_comments) {
384 0         0 $comments =~ s/^/-- /mg;
385 0         0 push @comments, "-- Comments:\n$comments\n--\n";
386             }
387             }
388              
389             #
390             # Fields
391             #
392 24         128 for my $field ($table->get_fields) {
393 74         748 push @field_defs,
394             create_field(
395             $field,
396             {
397             generator => $generator,
398             postgres_version => $postgres_version,
399             type_defs => $type_defs,
400             constraint_defs => \@constraint_defs,
401             attach_comments => $attach_comments
402             }
403             );
404 74 100       536 if ($attach_comments) {
405 12         557 my $field_comments = $field->comments;
406 12 100       153 next unless $field_comments;
407 2         55 my $field_name_qt = $generator->quote($field->name);
408 2         10 my $comment_ddl = "COMMENT on COLUMN $table_name_qt.$field_name_qt IS \$comment\$$field_comments\$comment\$";
409 2         10 push @comment_statements, $comment_ddl;
410             }
411              
412             }
413              
414             #
415             # Index Declarations
416             #
417 24         184 for my $index ($table->get_indices) {
418 5         100 my ($idef, $constraints) = create_index(
419             $index,
420             {
421             generator => $generator,
422             postgres_version => $postgres_version,
423             }
424             );
425 5 50       68 $idef and push @index_defs, $idef;
426 5         20 push @constraint_defs, @$constraints;
427             }
428              
429             #
430             # Table constraints
431             #
432 24         128 for my $c ($table->get_constraints) {
433 35         416 my ($cdefs, $fks) = create_constraint(
434             $c,
435             {
436             generator => $generator,
437             }
438             );
439 35         151 push @constraint_defs, @$cdefs;
440 35         135 push @fks, @$fks;
441             }
442              
443 24         100 my $create_statement = join("\n", @comments);
444 24 100       155 if ($add_drop_table) {
445 20 50       103 if ($postgres_version >= 8.002) {
446 0         0 $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
447             } else {
448 20         67 $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
449             }
450             }
451 24 50       943 my $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
452             $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n"
453 24         122 . join(",\n", map {" $_"} @field_defs, @constraint_defs) . "\n)";
  97         413  
454 24 100       163 $create_statement .= @index_defs ? ';' : q{};
455 24 100       180 $create_statement .= ($create_statement =~ /;$/ ? "\n" : q{}) . join(";\n", @index_defs);
456              
457             #
458             # Geometry
459             #
460 24 100       128 if (my @geometry_columns = grep { is_geometry($_) } $table->get_fields) {
  74         228  
461 0         0 $create_statement .= join(";\n", '', map { drop_geometry_column($_, $options) } @geometry_columns)
462 1 50       4 if $options->{add_drop_table};
463 1         3 $create_statement .= join(";\n", '', map { add_geometry_column($_, $options) } @geometry_columns);
  1         7  
464             }
465              
466 24 100       119 if (@comment_statements) {
467 2         11 $create_statement .= join(";\n", '', @comment_statements);
468             }
469              
470 24         276 return $create_statement, \@fks;
471             }
472              
473             sub create_view {
474 10     10 1 1475 my ($view, $options) = @_;
475 10         38 my $generator = _generator($options);
476 10   100     71 my $postgres_version = $options->{postgres_version} || 0;
477 10         33 my $add_drop_view = $options->{add_drop_view};
478              
479 10         49 my $view_name = $view->name;
480 10         72 debug("PKG: Looking at view '${view_name}'\n");
481              
482 10         23 my $create = '';
483             $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
484 10 50       50 unless $options->{no_comments};
485 10 100       37 if ($add_drop_view) {
486 7 100       33 if ($postgres_version >= 8.002) {
487 1         7 $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
488             } else {
489 6         37 $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
490             }
491             }
492 10         28 $create .= 'CREATE';
493              
494 10         271 my $extra = $view->extra;
495             $create .= " TEMPORARY"
496 10 50 66     51 if exists($extra->{temporary}) && $extra->{temporary};
497             $create .= " MATERIALIZED"
498 10 50 66     41 if exists($extra->{materialized}) && $extra->{materialized};
499 10         36 $create .= " VIEW " . $generator->quote($view_name);
500              
501 10 100       275 if (my @fields = $view->fields) {
502 9         23 my $field_list = join ', ', map { $generator->quote($_) } @fields;
  13         43  
503 9         30 $create .= " ( ${field_list} )";
504             }
505              
506 10 50       58 if (my $sql = $view->sql) {
507 10         28 $create .= " AS\n ${sql}\n";
508             }
509              
510 10 100       37 if ($extra->{check_option}) {
511 1         6 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
512             }
513              
514 10         74 return $create;
515             }
516              
517             # Returns a enum custom type name and list of values iff the field looks like an enum.
518             sub _enum_typename_and_values {
519 148     148   578 my $field = shift;
520 148 100       4002 if (ref $field->extra->{list} eq 'ARRAY') { # can't do anything unless we know the list
521 4 100       105 if ($field->extra->{custom_type_name}) {
    50          
522 3         77 return ($field->extra->{custom_type_name}, $field->extra->{list});
523             } elsif ($field->data_type eq 'enum') {
524 1         28 my $name = $field->table->name . '_' . $field->name . '_type';
525 1         80 return ($name, $field->extra->{list});
526             }
527             }
528 144         439 return ();
529             }
530              
531             {
532              
533             my %field_name_scope;
534              
535             sub create_field {
536 102     102 1 1358 my ($field, $options) = @_;
537              
538 102         352 my $generator = _generator($options);
539 102         3278 my $table_name = $field->table->name;
540 102   100     3537 my $constraint_defs = $options->{constraint_defs} || [];
541 102   100     627 my $postgres_version = $options->{postgres_version} || 0;
542 102   100     430 my $type_defs = $options->{type_defs} || {};
543 102         252 my $attach_comments = $options->{attach_comments};
544              
545 102   100     489 $field_name_scope{$table_name} ||= {};
546 102         3254 my $field_name = $field->name;
547              
548 102         2960 my $field_comments = '';
549 102 100 100     3042 if (!$attach_comments and my $comments = $field->comments) {
550 4         93 $comments =~ s/(?
551 4         17 $field_comments = "-- $comments\n ";
552             }
553              
554 102         1690 my $field_def = $field_comments . $generator->quote($field_name);
555              
556             #
557             # Datatype
558             #
559 102         569 my $data_type = lc $field->data_type;
560 102         2999 my %extra = $field->extra;
561 102         454 my ($enum_typename, $list) = _enum_typename_and_values($field);
562              
563 102 100 100     540 if ($postgres_version >= 8.003 && $enum_typename) {
564 2         8 my $commalist = join(', ', map { __PACKAGE__->_quote_string($_) } @$list);
  6         22  
565 2         10 $field_def .= ' ' . $enum_typename;
566 2         7 my $new_type_def
567             = "DROP TYPE IF EXISTS $enum_typename CASCADE;\n" . "CREATE TYPE $enum_typename AS ENUM ($commalist)";
568 2 50       9 if (!exists $type_defs->{$enum_typename}) {
    0          
569 2         9 $type_defs->{$enum_typename} = $new_type_def;
570             } elsif ($type_defs->{$enum_typename} ne $new_type_def) {
571 0         0 die "Attempted to redefine type name '$enum_typename' as a different type.\n";
572             }
573             } else {
574 100         384 $field_def .= ' ' . convert_datatype($field);
575             }
576              
577             #
578             # Default value
579             #
580 102         1221 __PACKAGE__->_apply_default_value(
581             $field,
582             \$field_def,
583             [
584             'NULL' => \'NULL',
585             'now()' => 'now()',
586             'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
587             ],
588             );
589              
590             #
591             # Not null constraint
592             #
593 102 100       3459 $field_def .= ' NOT NULL' unless $field->is_nullable;
594              
595             #
596             # Geometry constraints
597             #
598 102 100       6885 if (is_geometry($field)) {
599 3         14 foreach (create_geometry_constraints($field, $options)) {
600 9         29 my ($cdefs, $fks) = create_constraint($_, $options);
601 9         18 push @$constraint_defs, @$cdefs;
602 9         19 push @$fks, @$fks;
603             }
604             }
605              
606 102         752 return $field_def;
607             }
608             }
609              
610             sub create_geometry_constraints {
611 7     7 0 18 my ($field, $options) = @_;
612              
613 7         23 my $fname = _generator($options)->quote($field);
614 7         17 my @constraints;
615             push @constraints,
616             SQL::Translator::Schema::Constraint->new(
617             name => "enforce_dims_" . $field->name,
618 7         121 expression => "(ST_NDims($fname) = " . $field->extra->{dimensions} . ")",
619             table => $field->table,
620             type => CHECK_C,
621             );
622              
623             push @constraints,
624             SQL::Translator::Schema::Constraint->new(
625             name => "enforce_srid_" . $field->name,
626 7         282 expression => "(ST_SRID($fname) = " . $field->extra->{srid} . ")",
627             table => $field->table,
628             type => CHECK_C,
629             );
630             push @constraints,
631             SQL::Translator::Schema::Constraint->new(
632             name => "enforce_geotype_" . $field->name,
633             expression => "(GeometryType($fname) = "
634             . __PACKAGE__->_quote_string($field->extra->{geometry_type})
635 7         247 . "::text OR $fname IS NULL)",
636             table => $field->table,
637             type => CHECK_C,
638             );
639              
640 7         181 return @constraints;
641             }
642              
643             sub _extract_extras_from_options {
644 88     88   238 my ($options_haver, $dispatcher) = @_;
645 88         2584 for my $opt ($options_haver->options) {
646 7 50       28 if (ref $opt eq 'HASH') {
647 7         20 for my $key (keys %$opt) {
648 8         13 my $val = $opt->{$key};
649 8 50       23 next unless defined $val;
650 8         23 $dispatcher->{ lc $key }->($val);
651             }
652             }
653             }
654             }
655              
656             {
657             my %index_name;
658              
659             sub create_index {
660 18     18 1 5638 my ($index, $options) = @_;
661              
662 18         66 my $generator = _generator($options);
663 18         680 my $table_name = $index->table->name;
664 18   100     606 my $postgres_version = $options->{postgres_version} || 0;
665              
666 18         44 my ($index_def, @constraint_defs);
667              
668             my $name = $index->name
669 18   33     602 || join('_', $table_name, 'idx', ++$index_name{$table_name});
670              
671 18   50     656 my $type = $index->type || NORMAL;
672 18         984 my @fields = $index->fields;
673 18 50       72 return unless @fields;
674              
675 18         41 my %index_extras;
676             _extract_extras_from_options(
677             $index,
678             {
679 2     2   7 using => sub { $index_extras{using} = "USING $_[0]" },
680 2     2   7 where => sub { $index_extras{where} = "WHERE $_[0]" },
681             include => sub {
682 2     2   6 my ($value) = @_;
683 2 100       10 return unless $postgres_version >= 11;
684 1 50       5 die 'Include list must be an arrayref'
685             unless ref $value eq 'ARRAY';
686 1         5 my $value_list = join ', ', @$value;
687 1         5 $index_extras{include} = "INCLUDE ($value_list)";
688             }
689             }
690 18         307 );
691              
692 18         266 my $def_start = 'CONSTRAINT ' . $generator->quote($name) . ' ';
693 18 100       57 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ($generator->quote($_)) } @fields)) . ')';
  22         385  
694 18 50       2109 if ($type eq PRIMARY_KEY) {
    100          
    50          
695 0         0 push @constraint_defs, "${def_start}PRIMARY KEY " . $field_names;
696             } elsif ($type eq UNIQUE) {
697 1         6 push @constraint_defs, "${def_start}UNIQUE " . $field_names;
698             } elsif ($type eq NORMAL) {
699             $index_def
700             = 'CREATE INDEX ' . $generator->quote($name) . ' on ' . $generator->quote($table_name) . ' ' . join ' ',
701 68         173 grep {defined} $index_extras{using}, $field_names,
702 17         62 @index_extras{ 'include', 'where' };
703             } else {
704 0 0       0 warn "Unknown index type ($type) on table $table_name.\n"
705             if $WARN;
706             }
707              
708 18         130 return $index_def, \@constraint_defs;
709             }
710             }
711              
712             sub create_constraint {
713 70     70 1 3499 my ($c, $options) = @_;
714              
715 70         233 my $generator = _generator($options);
716 70   50     400 my $postgres_version = $options->{postgres_version} || 0;
717 70         2185 my $table_name = $c->table->name;
718 70         1996 my (@constraint_defs, @fks);
719 70         0 my %constraint_extras;
720             _extract_extras_from_options(
721             $c,
722             {
723 1     1   23 using => sub { $constraint_extras{using} = "USING $_[0]" },
724 1     1   5 where => sub { $constraint_extras{where} = "WHERE ( $_[0] )" },
725             include => sub {
726 0     0   0 my ($value) = @_;
727 0 0       0 return unless $postgres_version >= 11;
728 0 0       0 die 'Include list must be an arrayref'
729             unless ref $value eq 'ARRAY';
730 0         0 my $value_list = join ', ', @$value;
731 0         0 $constraint_extras{include} = "INCLUDE ( $value_list )";
732             },
733             }
734 70         1008 );
735              
736 70   100     2545 my $name = $c->name || '';
737              
738 70         317 my @fields = grep {defined} $c->fields;
  55         258  
739              
740 70         2115 my @rfields = grep {defined} $c->reference_fields;
  8         43  
741              
742 70 50 66     539 return if !@fields && ($c->type ne CHECK_C && $c->type ne EXCLUDE);
      66        
743 70 100       634 my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) : '';
744 70 100       227 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ($generator->quote($_)) } @fields)) . ')';
  55         413  
745 70   50     563 my $include = $constraint_extras{include} || '';
746 70 100       1924 if ($c->type eq PRIMARY_KEY) {
    100          
    100          
    100          
    100          
747 17         741 push @constraint_defs, join ' ', grep $_, $def_start, "PRIMARY KEY", $field_names, $include;
748             } elsif ($c->type eq UNIQUE) {
749 16         556 push @constraint_defs, join ' ', grep $_, $def_start, "UNIQUE", $field_names, $include;
750             } elsif ($c->type eq CHECK_C) {
751 18         467 my $expression = $c->expression;
752 18         91 push @constraint_defs, join ' ', grep $_, $def_start, "CHECK ($expression)";
753             } elsif ($c->type eq FOREIGN_KEY) {
754 8         275 my $def .= join ' ', grep $_, "ALTER TABLE",
755             $generator->quote($table_name), 'ADD', $def_start,
756             "FOREIGN KEY $field_names";
757 8         70 $def .= "\n REFERENCES " . $generator->quote($c->reference_table);
758              
759 8 50       95 if (@rfields) {
760 8         26 $def .= ' (' . join(', ', map { $generator->quote($_) } @rfields) . ')';
  8         42  
761             }
762              
763 8 50       260 if ($c->match_type) {
764 0 0       0 $def .= ' MATCH ' . ($c->match_type =~ /full/i) ? 'FULL' : 'PARTIAL';
765             }
766              
767 8 50       496 if ($c->on_delete) {
768 0         0 $def .= ' ON DELETE ' . $c->on_delete;
769             }
770              
771 8 50       243 if ($c->on_update) {
772 0         0 $def .= ' ON UPDATE ' . $c->on_update;
773             }
774              
775 8 50       358 if ($c->deferrable) {
776 8         100 $def .= ' DEFERRABLE';
777             }
778              
779 8         41 push @fks, "$def";
780             } elsif ($c->type eq EXCLUDE) {
781 1   50     30 my $using = $constraint_extras{using} || '';
782 1         30 my $expression = $c->expression;
783 1   50     5 my $where = $constraint_extras{where} || '';
784 1         9 push @constraint_defs, join ' ', grep $_, $def_start, 'EXCLUDE', $using, "( $expression )", $include, $where;
785             }
786              
787 70         834 return \@constraint_defs, \@fks;
788             }
789              
790             sub create_trigger {
791 14     14 1 40 my ($trigger, $options) = @_;
792 14         49 my $generator = _generator($options);
793              
794 14         28 my @statements;
795              
796             push @statements, sprintf('DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name))
797 14 50       140 if $options->{add_drop_trigger};
798              
799 14   100     429 my $scope = $trigger->scope || '';
800 14 100       459 $scope = " FOR EACH $scope" if $scope;
801              
802             push @statements,
803             sprintf(
804             'CREATE TRIGGER %s %s %s ON %s%s %s',
805             $generator->quote($trigger->name),
806             $trigger->perform_action_when,
807 14         120 join(' OR ', @{ $trigger->database_events }),
  14         651  
808             $generator->quote($trigger->on_table),
809             $scope, $trigger->action,
810             );
811              
812 14         130 return @statements;
813             }
814              
815             sub convert_datatype {
816 146     146 0 327 my ($field) = @_;
817              
818 146         3950 my @size = $field->size;
819 146         2028 my $data_type = lc $field->data_type;
820 146         416 my $array = $data_type =~ s/\[\]$//;
821              
822 146 100       3596 if ($data_type eq 'enum') {
    50          
    100          
823              
824             # my $len = 0;
825             # $len = ($len < length($_)) ? length($_) : $len for (@$list);
826             # my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
827             # push @$constraint_defs,
828             # 'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
829             # qq[IN ($commalist))];
830 2         5 $data_type = 'character varying';
831             } elsif ($data_type eq 'set') {
832 0         0 $data_type = 'character varying';
833             } elsif ($field->is_auto_increment) {
834 19 100 66     418 if ((defined $size[0] && $size[0] > 11) or $data_type eq 'bigint') {
      100        
835 2         6 $data_type = 'bigserial';
836             } else {
837 17         56 $data_type = 'serial';
838             }
839 19         62 undef @size;
840             } else {
841             $data_type
842             = defined $translate{ lc $data_type }
843 125 100       1933 ? $translate{ lc $data_type }
844             : $data_type;
845             }
846              
847 146 100 100     1043 if ($data_type =~ /^time/i || $data_type =~ /^interval/i) {
848 13 100 66     110 if (defined $size[0] && $size[0] > 6) {
849 1         3 $size[0] = 6;
850             }
851             }
852              
853 146 100       508 if ($data_type eq 'integer') {
854 17 100 66     139 if (defined $size[0] && $size[0] > 0) {
855 15 100       57 if ($size[0] > 10) {
    50          
856 10         20 $data_type = 'bigint';
857             } elsif ($size[0] < 5) {
858 0         0 $data_type = 'smallint';
859             } else {
860 5         23 $data_type = 'integer';
861             }
862             } else {
863 2         5 $data_type = 'integer';
864             }
865             }
866              
867 146         359 my $type_with_size = join('|',
868             'bit', 'varbit', 'character', 'bit varying', 'character varying',
869             'time', 'timestamp', 'interval', 'numeric', 'float');
870              
871 146 100       1859 if ($data_type !~ /$type_with_size/) {
872 60         167 @size = ();
873             }
874              
875 146 100 100     1548 if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i) {
    100 100        
      100        
876 5         60 $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
877 5 50       29 $data_type .= $2 if (defined $2);
878             } elsif (defined $size[0] && $size[0] > 0) {
879 69         298 $data_type .= '(' . join(',', @size) . ')';
880             }
881 146 100       424 if ($array) {
882 1         4 $data_type .= '[]';
883             }
884              
885             #
886             # Geography
887             #
888 146 100       499 if ($data_type eq 'geography') {
889 1         16 $data_type .= '(' . $field->extra->{geography_type} . ',' . $field->extra->{srid} . ')';
890             }
891              
892 146         653 return $data_type;
893             }
894              
895             sub alter_field {
896 23     23 1 8530 my ($from_field, $to_field, $options) = @_;
897              
898 23 50       654 die "Can't alter field in another table"
899             if ($from_field->table->name ne $to_field->table->name);
900              
901 23         509 my $generator = _generator($options);
902 23         67 my @out;
903              
904             # drop geometry column and constraints
905 23 100       90 push @out, drop_geometry_column($from_field, $options), drop_geometry_constraints($from_field, $options),
906             if is_geometry($from_field);
907              
908             # it's necessary to start with rename column cause this would affect
909             # all of the following statements which would be broken if do the
910             # rename later
911             # BUT: drop geometry is done before the rename, cause it work's on the
912             # $from_field directly
913 23 100       591 push @out,
914             sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
915             map($generator->quote($_), $to_field->table->name, $from_field->name, $to_field->name,),)
916             if ($from_field->name ne $to_field->name);
917              
918 23 100 100     916 push @out,
919             sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
920             map($generator->quote($_), $to_field->table->name, $to_field->name),)
921             if (!$to_field->is_nullable and $from_field->is_nullable);
922              
923 23 100 100     942 push @out,
924             sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
925             map($generator->quote($_), $to_field->table->name, $to_field->name),)
926             if (!$from_field->is_nullable and $to_field->is_nullable);
927              
928 23         727 my $from_dt = convert_datatype($from_field);
929 23         57 my $to_dt = convert_datatype($to_field);
930 23 100       257 push @out,
931             sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
932             map($generator->quote($_), $to_field->table->name, $to_field->name), $to_dt,)
933             if ($to_dt ne $from_dt);
934              
935 23         139 my ($from_enum_typename, $from_list) = _enum_typename_and_values($from_field);
936 23         55 my ($to_enum_typename, $to_list) = _enum_typename_and_values($to_field);
937 23 50 66     82 if ( $from_enum_typename
      66        
938             && $to_enum_typename
939             && $from_enum_typename eq $to_enum_typename) {
940             # See if new enum values were added, and update the enum
941 1         10 my %existing_vals = map +($_ => 1), @$from_list;
942 1         7 my %desired_vals = map +($_ => 1), @$to_list;
943 1         8 my @add_vals = grep !$existing_vals{$_}, keys %desired_vals;
944 1         6 my @del_vals = grep !$desired_vals{$_}, keys %existing_vals;
945 1   50     6 my $pg_ver_ok = ($options->{postgres_version} || 0) >= 9.001;
946 1 50 33     6 push @out, '-- Set $sqlt->producer_args->{postgres_version} >= 9.001 to alter enums'
947             if !$pg_ver_ok && @add_vals;
948 1         4 for (@add_vals) {
949 1 50       9 push @out, sprintf '%sALTER TYPE %s ADD VALUE IF NOT EXISTS %s',
950             ($pg_ver_ok ? '' : '-- '), $to_enum_typename,
951             $generator->quote_string($_);
952             }
953 1 50       7 push @out, "-- Unimplemented: delete values from enum type '$to_enum_typename': " . join(", ", @del_vals)
954             if @del_vals;
955             }
956              
957 23         72 my $old_default = $from_field->default_value;
958 23         52 my $new_default = $to_field->default_value;
959 23         48 my $default_value = $to_field->default_value;
960              
961             # fixes bug where output like this was created:
962             # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
963 23 100 100     140 if (ref $default_value eq "SCALAR") {
    100          
964 1         4 $default_value = $$default_value;
965             } elsif (defined $default_value
966             && $to_dt =~ /^(character|text|timestamp|date)/xsmi) {
967 3         23 $default_value = __PACKAGE__->_quote_string($default_value);
968             }
969              
970 23 100 100     208 push @out,
      100        
971             sprintf(
972             'ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
973             map($generator->quote($_), $to_field->table->name, $to_field->name,),
974             $default_value,
975             )
976             if (defined $new_default
977             && (!defined $old_default || $old_default ne $new_default));
978              
979             # fixes bug where removing the DEFAULT statement of a column
980             # would result in no change
981              
982 23 100 100     181 push @out,
983             sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
984             map($generator->quote($_), $to_field->table->name, $to_field->name,),)
985             if (!defined $new_default && defined $old_default);
986              
987             # add geometry column and constraints
988 23 100       65 push @out, add_geometry_column($to_field, $options), add_geometry_constraints($to_field, $options),
989             if is_geometry($to_field);
990              
991 23 100       163 return wantarray ? @out : join(";\n", @out);
992             }
993              
994 2     2 0 10 sub rename_field { alter_field(@_) }
995              
996             sub add_field {
997 6     6 1 78 my ($new_field, $options) = @_;
998              
999 6         20 my $out = sprintf(
1000             'ALTER TABLE %s ADD COLUMN %s',
1001             _generator($options)->quote($new_field->table->name),
1002             create_field($new_field, $options)
1003             );
1004 6 100       24 $out .= ";\n" . add_geometry_column($new_field, $options) . ";\n" . add_geometry_constraints($new_field, $options)
1005             if is_geometry($new_field);
1006 6         38 return $out;
1007              
1008             }
1009              
1010             sub drop_field {
1011 4     4 1 2529 my ($old_field, $options) = @_;
1012              
1013 4         34 my $generator = _generator($options);
1014              
1015 4         163 my $out = sprintf(
1016             'ALTER TABLE %s DROP COLUMN %s',
1017             $generator->quote($old_field->table->name),
1018             $generator->quote($old_field->name)
1019             );
1020 4 100       37 $out .= ";\n" . drop_geometry_column($old_field, $options)
1021             if is_geometry($old_field);
1022 4         32 return $out;
1023             }
1024              
1025             sub add_geometry_column {
1026 5     5 0 1163 my ($field, $options) = @_;
1027              
1028             return sprintf(
1029             "INSERT INTO geometry_columns VALUES (%s,%s,%s,%s,%s,%s,%s)",
1030             map(__PACKAGE__->_quote_string($_),
1031             '',
1032             $field->table->schema->name,
1033             $options->{table} ? $options->{table} : $field->table->name,
1034             $field->name,
1035             $field->extra->{dimensions},
1036             $field->extra->{srid},
1037             $field->extra->{geometry_type},
1038 5 100       99 ),
1039             );
1040             }
1041              
1042             sub drop_geometry_column {
1043 4     4 0 23 my ($field) = @_;
1044              
1045             return
1046 4         62 sprintf("DELETE FROM geometry_columns WHERE f_table_schema = %s AND f_table_name = %s AND f_geometry_column = %s",
1047             map(__PACKAGE__->_quote_string($_), $field->table->schema->name, $field->table->name, $field->name,),);
1048             }
1049              
1050             sub add_geometry_constraints {
1051 3     3 0 608 my ($field, $options) = @_;
1052              
1053 3         13 return join(";\n", map { alter_create_constraint($_, $options) } create_geometry_constraints($field, $options));
  9         30  
1054             }
1055              
1056             sub drop_geometry_constraints {
1057 1     1 0 4 my ($field, $options) = @_;
1058              
1059 1         6 return join(";\n", map { alter_drop_constraint($_, $options) } create_geometry_constraints($field, $options));
  3         13  
1060              
1061             }
1062              
1063             sub alter_table {
1064 3     3 0 12 my ($to_table, $options) = @_;
1065 3         8 my $generator = _generator($options);
1066 3         96 my $out = sprintf('ALTER TABLE %s %s', $generator->quote($to_table->name), $options->{alter_table_action});
1067             $out .= ";\n" . $options->{geometry_changes}
1068 3 100       18 if $options->{geometry_changes};
1069 3         13 return $out;
1070             }
1071              
1072             sub rename_table {
1073 3     3 0 1050 my ($old_table, $new_table, $options) = @_;
1074 3         9 my $generator = _generator($options);
1075 3         15 $options->{alter_table_action} = "RENAME TO " . $generator->quote($new_table);
1076              
1077             my @geometry_changes
1078 1         5 = map { drop_geometry_column($_, $options), add_geometry_column($_, { %{$options}, table => $new_table }), }
  1         5  
1079 3         38 grep { is_geometry($_) } $old_table->get_fields;
  4         10  
1080              
1081 3 100       13 $options->{geometry_changes} = join(";\n", @geometry_changes)
1082             if @geometry_changes;
1083              
1084 3         12 return alter_table($old_table, $options);
1085             }
1086              
1087             sub alter_create_index {
1088 1     1 0 6 my ($index, $options) = @_;
1089 1         4 my $generator = _generator($options);
1090 1         7 my ($idef, $constraints) = create_index($index, $options);
1091 1 50       31 return $index->type eq NORMAL
1092             ? $idef
1093             : sprintf('ALTER TABLE %s ADD %s', $generator->quote($index->table->name), join(q{}, @$constraints));
1094             }
1095              
1096             sub alter_drop_index {
1097 1     1 0 2 my ($index, $options) = @_;
1098 1         4 return 'DROP INDEX ' . _generator($options)->quote($index->name);
1099             }
1100              
1101             sub alter_drop_constraint {
1102 14     14 0 8353 my ($c, $options) = @_;
1103 14         43 my $generator = _generator($options);
1104              
1105             # NOT NULL constraint does not require a DROP CONSTRAINT statement
1106 14 100       435 if ($c->type eq NOT_NULL) {
1107 2         40 return;
1108             }
1109              
1110             # attention: Postgres has a very special naming structure for naming
1111             # foreign keys and primary keys. It names them using the name of the
1112             # table as prefix and fkey or pkey as suffix, concatenated by an underscore
1113 12         328 my $c_name;
1114 12 100       377 if ($c->name) {
1115              
1116             # Already has a name, just use it
1117 9         211 $c_name = $c->name;
1118             } else {
1119             # if the name is dotted we need the table, not schema nor database
1120 3         86 my ($tablename) = reverse split /[.]/, $c->table->name;
1121 3 100       213 if ($c->type eq FOREIGN_KEY) {
    50          
1122              
1123             # Doesn't have a name, and is foreign key, append '_fkey'
1124 1         34 $c_name = $tablename . '_' . ($c->fields)[0] . '_fkey';
1125             } elsif ($c->type eq PRIMARY_KEY) {
1126              
1127             # Doesn't have a name, and is primary key, append '_pkey'
1128 2         81 $c_name = $tablename . '_pkey';
1129             }
1130             }
1131              
1132 12         270 return sprintf('ALTER TABLE %s DROP CONSTRAINT %s', map { $generator->quote($_) } $c->table->name, $c_name,);
  24         361  
1133             }
1134              
1135             sub alter_create_constraint {
1136 14     14 0 58 my ($index, $options) = @_;
1137 14         31 my $generator = _generator($options);
1138 14         70 my ($defs, $fks) = create_constraint(@_);
1139              
1140             # return if there are no constraint definitions so we don't run
1141             # into output like this:
1142             # ALTER TABLE users ADD ;
1143              
1144 14 50 66     27 return unless (@{$defs} || @{$fks});
  14         61  
  1         3  
1145             return $index->type eq FOREIGN_KEY
1146 1         24 ? join(q{}, @{$fks})
1147 14 100       269 : join(' ', 'ALTER TABLE', $generator->quote($index->table->name), 'ADD', join(q{}, @{$defs}, @{$fks}));
  13         30  
  13         130  
1148             }
1149              
1150             sub drop_table {
1151 3     3 0 45 my ($table, $options) = @_;
1152 3         11 my $generator = _generator($options);
1153 3         16 my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
1154              
1155 1         3 my @geometry_drops = map { drop_geometry_column($_); }
1156 3         48 grep { is_geometry($_) } $table->get_fields;
  4         13  
1157              
1158 3 100       15 $out .= join(";\n", '', @geometry_drops) if @geometry_drops;
1159 3         19 return $out;
1160             }
1161              
1162             sub batch_alter_table {
1163 12     12 0 461 my ($table, $diff_hash, $options) = @_;
1164              
1165             # as long as we're not renaming the table we don't need to be here
1166 12 100       19 if (@{ $diff_hash->{rename_table} } == 0) {
  12         37  
1167 10         35 return batch_alter_table_statements($diff_hash, $options);
1168             }
1169              
1170             # first we need to perform drops which are on old table
1171 2         9 my @sql = batch_alter_table_statements(
1172             $diff_hash, $options, qw(
1173             alter_drop_constraint
1174             alter_drop_index
1175             drop_field
1176             )
1177             );
1178              
1179             # next comes the rename_table
1180 2         6 my $old_table = $diff_hash->{rename_table}[0][0];
1181 2         8 push @sql, rename_table($old_table, $table, $options);
1182              
1183             # for alter_field (and so also rename_field) we need to make sure old
1184             # field has table name set to new table otherwise calling alter_field dies
1185 2 0       4 $diff_hash->{alter_field} = [ map { $_->[0]->table($table) && $_ } @{ $diff_hash->{alter_field} } ];
  0         0  
  2         7  
1186 2 0       5 $diff_hash->{rename_field} = [ map { $_->[0]->table($table) && $_ } @{ $diff_hash->{rename_field} } ];
  0         0  
  2         5  
1187              
1188             # now add everything else
1189 2         9 push @sql, batch_alter_table_statements(
1190             $diff_hash, $options, qw(
1191             add_field
1192             alter_field
1193             rename_field
1194             alter_create_index
1195             alter_create_constraint
1196             alter_table
1197             )
1198             );
1199              
1200 2         18 return @sql;
1201             }
1202              
1203             1;
1204              
1205             # -------------------------------------------------------------------
1206             # Life is full of misery, loneliness, and suffering --
1207             # and it's all over much too soon.
1208             # Woody Allen
1209             # -------------------------------------------------------------------
1210              
1211             =pod
1212              
1213             =head1 SEE ALSO
1214              
1215             SQL::Translator, SQL::Translator::Producer::Oracle.
1216              
1217             =head1 AUTHOR
1218              
1219             Ken Youens-Clark Ekclark@cpan.orgE.
1220              
1221             =cut