Skip to content

Commit

Permalink
Support EXCLUDE constraints in Postgres (#168)
Browse files Browse the repository at this point in the history
* Implement EXCLUDE constraint for PostgreSQL

* finish support + test EXCLUDE on PG

* feat: add parser support for EXCLUDE, with WHERE INCLUDE and USING support

---------

Co-authored-by: Eugen Konkov <[email protected]>
  • Loading branch information
rabbiveesh and KES777 authored Dec 22, 2023
1 parent dee6a8d commit 7db8c69
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 225 deletions.
42 changes: 29 additions & 13 deletions lib/SQL/Translator/Parser/PostgreSQL.pm
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,19 @@ Table:
CHECK (expression) |
REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
[ ON DELETE action ] [ ON UPDATE action ] }
[ DEFERRABLE | NOT DEFERRABLE ]
[ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
and table_constraint is:
[ CONSTRAINT constraint_name ]
{ UNIQUE ( column_name [, ... ] ) |
PRIMARY KEY ( column_name [, ... ] ) |
CHECK ( expression ) |
EXCLUDE [USING acc_method] (expression) [INCLUDE (column [, ...])] [WHERE (predicate)]
FOREIGN KEY ( column_name [, ... ] )
REFERENCES reftable [ ( refcolumn [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL ]
[ ON DELETE action ] [ ON UPDATE action ] }
[ DEFERRABLE | NOT DEFERRABLE ]
[ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
[ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
Index :
(http://www.postgresql.org/docs/current/sql-createindex.html)
Expand Down Expand Up @@ -317,6 +315,8 @@ using_method : /using/i WORD { $item[2] }
where_predicate : /where/i /[^;]+/
where_paren_predicate : /where/i '(' /[^;]+/ ')'
include_covering : /include/i '(' covering_field_name(s /,/) ')'
{ $item{'covering_field_name(s)'} }
Expand Down Expand Up @@ -679,37 +679,38 @@ table_constraint : comment(s?) constraint_name(?) table_constraint_type deferrab
my $fields = $desc->{'fields'};
my $expression = $desc->{'expression'};
my @comments = ( @{ $item[1] }, @{ $item[-1] } );
my $expr_constraint = $type eq 'check' || $type eq 'exclude';
$return = {
name => $item[2][0] || '',
supertype => 'constraint',
type => $type,
fields => $type ne 'check' ? $fields : [],
expression => $type eq 'check' ? $expression : '',
fields => $expr_constraint ? [] : $fields,
expression => $expr_constraint ? $expression : '',
deferrable => $item{'deferrable'},
deferred => $item{'deferred'},
reference_table => $desc->{'reference_table'},
reference_fields => $desc->{'reference_fields'},
match_type => $desc->{'match_type'},
on_delete => $desc->{'on_delete'} || $desc->{'on_delete_do'},
on_update => $desc->{'on_update'} || $desc->{'on_update_do'},
comments => [ @comments ],
%{$desc}{qw/include using where reference_table reference_fields match_type/}
}
}
table_constraint_type : /primary key/i '(' NAME(s /,/) ')'
table_constraint_type : /primary key/i '(' NAME(s /,/) ')' include_covering(?)
{
$return = {
type => 'primary_key',
fields => $item[3],
include => $item{'include_convering(?)'}[0],
}
}
|
/unique/i '(' NAME(s /,/) ')'
/unique/i '(' NAME(s /,/) ')' include_covering(?)
{
$return = {
type => 'unique',
fields => $item[3],
include => $item{'include_convering(?)'}[0],
}
}
|
Expand All @@ -721,6 +722,16 @@ table_constraint_type : /primary key/i '(' NAME(s /,/) ')'
}
}
|
/exclude/i using_method(?) '(' /[^)]+/ ')' include_covering(?) where_paren_predicate(?) {
$return = {
type => 'exclude',
expression => $item{__PATTERN2__},
using => $item{'using_method(?)'}[0],
include => $item{'include_convering(?)'}[0],
where => $item{'where_paren_predicate(?)'}[0],
}
}
|
/foreign key/i '(' NAME(s /,/) ')' /references/i table_id parens_word_list(?) match_type(?) key_action(s?)
{
my ( $on_delete, $on_update );
Expand Down Expand Up @@ -1119,6 +1130,10 @@ sub parse {
}

for my $cdata ( @{ $tdata->{'constraints'} || [] } ) {
my $options = [
# load this up with the extras
map +{%{$cdata}{$_}}, grep $cdata->{$_}, qw/include using where/
];
my $constraint = $table->add_constraint(
name => $cdata->{'name'},
type => $cdata->{'type'},
Expand All @@ -1129,6 +1144,7 @@ sub parse {
on_delete => $cdata->{'on_delete'} || $cdata->{'on_delete_do'},
on_update => $cdata->{'on_update'} || $cdata->{'on_update_do'},
expression => $cdata->{'expression'},
options => $options
) or die "Can't add constraint of type '" .
$cdata->{'type'} . "' to table '" . $table->name .
"': " . $table->error;
Expand Down
77 changes: 51 additions & 26 deletions lib/SQL/Translator/Producer/PostgreSQL.pm
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ and table_constraint is:
{ UNIQUE ( column_name [, ... ] ) |
PRIMARY KEY ( column_name [, ... ] ) |
CHECK ( expression ) |
EXCLUDE [USING acc_method] (expression) [INCLUDE (column [, ...])] [WHERE (predicate)]
FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
Expand Down Expand Up @@ -615,6 +616,19 @@ sub create_geometry_constraints {
return @constraints;
}

sub _extract_extras_from_options {
my ($options_haver, $dispatcher) = @_;
for my $opt ($options_haver->options) {
if (ref $opt eq 'HASH') {
for my $key (keys %$opt) {
my $val = $opt->{$key};
next unless defined $val;
$dispatcher->{lc $key}->($val);
}
}
}
}

{
my %index_name;
sub create_index
Expand All @@ -636,26 +650,17 @@ sub create_geometry_constraints {
return unless @fields;

my %index_extras;
for my $opt ( $index->options ) {
if ( ref $opt eq 'HASH' ) {
foreach my $key (keys %$opt) {
my $value = $opt->{$key};
next unless defined $value;
if ( uc($key) eq 'USING' ) {
$index_extras{using} = "USING $value";
}
elsif ( uc($key) eq 'WHERE' ) {
$index_extras{where} = "WHERE $value";
}
elsif ( uc($key) eq 'INCLUDE' ) {
next unless $postgres_version >= 11;
die 'Include list must be an arrayref' unless ref $value eq 'ARRAY';
my $value_list = join ', ', @$value;
$index_extras{include} = "INCLUDE ($value_list)"
}
}
_extract_extras_from_options($index, {
using => sub { $index_extras{using} = "USING $_[0]" },
where => sub { $index_extras{where} = "WHERE $_[0]" },
include => sub {
my ($value) = @_;
return unless $postgres_version >= 11;
die 'Include list must be an arrayref' unless ref $value eq 'ARRAY';
my $value_list = join ', ', @$value;
$index_extras{include} = "INCLUDE ($value_list)"
}
}
});

my $def_start = 'CONSTRAINT ' . $generator->quote($name) . ' ';
my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
Expand Down Expand Up @@ -684,31 +689,45 @@ sub create_constraint
my ($c, $options) = @_;

my $generator = _generator($options);
my $postgres_version = $options->{postgres_version} || 0;
my $table_name = $c->table->name;
my (@constraint_defs, @fks);
my %constraint_extras;
_extract_extras_from_options($c, {
using => sub { $constraint_extras{using} = "USING $_[0]" },
where => sub { $constraint_extras{where} = "WHERE ( $_[0] )" },
include => sub {
my ($value) = @_;
return unless $postgres_version >= 11;
die 'Include list must be an arrayref' unless ref $value eq 'ARRAY';
my $value_list = join ', ', @$value;
$constraint_extras{include} = "INCLUDE ( $value_list )"
},
});

my $name = $c->name || '';

my @fields = grep { defined } $c->fields;

my @rfields = grep { defined } $c->reference_fields;

next if !@fields && $c->type ne CHECK_C;
my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
return if !@fields && ($c->type ne CHECK_C && $c->type ne EXCLUDE);
my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) : '';
my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
my $include = $constraint_extras{include} || '';
if ( $c->type eq PRIMARY_KEY ) {
push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
push @constraint_defs, join ' ', grep $_, $def_start, "PRIMARY KEY", $field_names, $include;
}
elsif ( $c->type eq UNIQUE ) {
push @constraint_defs, "${def_start}UNIQUE " .$field_names;
push @constraint_defs, join ' ', grep $_, $def_start, "UNIQUE", $field_names, $include;
}
elsif ( $c->type eq CHECK_C ) {
my $expression = $c->expression;
push @constraint_defs, "${def_start}CHECK ($expression)";
push @constraint_defs, join ' ', grep $_, $def_start, "CHECK ($expression)";
}
elsif ( $c->type eq FOREIGN_KEY ) {
my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
. "\n REFERENCES " . $generator->quote($c->reference_table);
my $def .= join ' ', grep $_, "ALTER TABLE", $generator->quote($table_name), 'ADD', $def_start, "FOREIGN KEY $field_names";
$def .= "\n REFERENCES " . $generator->quote($c->reference_table);

if ( @rfields ) {
$def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
Expand All @@ -733,6 +752,12 @@ sub create_constraint

push @fks, "$def";
}
elsif( $c->type eq EXCLUDE ) {
my $using = $constraint_extras{using} || '';
my $expression = $c->expression;
my $where = $constraint_extras{where} || '';
push @constraint_defs, join ' ', grep $_, $def_start, 'EXCLUDE', $using, "( $expression )", $include, $where;
}

return \@constraint_defs, \@fks;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/SQL/Translator/Schema/Constants.pm
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ This module exports the following constants for Schema features;
=item UNIQUE
=item EXCLUDE
=back
=cut
Expand All @@ -55,6 +57,7 @@ our @EXPORT = qw[
NULL
PRIMARY_KEY
UNIQUE
EXCLUDE
];

#
Expand All @@ -78,6 +81,8 @@ use constant PRIMARY_KEY => 'PRIMARY KEY';

use constant UNIQUE => 'UNIQUE';

use constant EXCLUDE => 'EXCLUDE';

1;

=pod
Expand Down
1 change: 1 addition & 0 deletions lib/SQL/Translator/Schema/Constraint.pm
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ my %VALID_CONSTRAINT_TYPE = (
CHECK_C, 1,
FOREIGN_KEY, 1,
NOT_NULL, 1,
EXCLUDE, 1,
);

=head2 new
Expand Down
Loading

0 comments on commit 7db8c69

Please sign in to comment.