Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[YSQL] DEFERRABLE constraint should be supported #1709

Open
ramanans opened this issue Jul 4, 2019 · 4 comments
Open

[YSQL] DEFERRABLE constraint should be supported #1709

ramanans opened this issue Jul 4, 2019 · 4 comments
Labels
area/ysql Yugabyte SQL (YSQL) community/request Issues created by external users kind/enhancement This is an enhancement of an existing feature priority/medium Medium priority issue
Projects

Comments

@ramanans
Copy link

ramanans commented Jul 4, 2019

Jira Link: DB-2287
ALTER TABLE ONLY public.abc
ADD CONSTRAINT abc_xyz UNIQUE (a, b) DEFERRABLE INITIALLY DEFERRED;

@m-iancu m-iancu self-assigned this Jul 4, 2019
@m-iancu m-iancu added this to To do in YSQL via automation Jul 4, 2019
@m-iancu m-iancu added the area/ysql Yugabyte SQL (YSQL) label Jul 4, 2019
@m-iancu m-iancu added this to the v2.0 milestone Jul 4, 2019
@yugabyte-ci yugabyte-ci added the community/request Issues created by external users label Jul 17, 2019
@ttyusupov ttyusupov added the kind/enhancement This is an enhancement of an existing feature label Jul 30, 2019
@frozenspider
Copy link
Contributor

frozenspider commented Jun 2, 2020

An interesting test case for UNIQUE + DEFERRABLE that should be supported:

CREATE TABLE t2(i int UNIQUE DEFERRABLE, v text);

INSERT INTO t2 VALUES (1, 'v1'), (2, 'v2'), (3, 'v3'), (4, 'v4'), (5, 'v5');

SELECT *, (i % 5 + 1) AS new_id FROM t2 ORDER BY i;
--  i | v  | new_id
-- ---+----+--------
--  1 | v1 |      2
--  2 | v2 |      3
--  3 | v3 |      4
--  4 | v4 |      5
--  5 | v5 |      1
-- (5 rows)

UPDATE t2 SET i = (i % 5 + 1);
-- ERROR:  duplicate key value violates unique constraint "t2_i_key"

@rkarthik007 rkarthik007 removed this from the v2.0 milestone Jun 5, 2020
@ndeodhar ndeodhar moved this from To do to Backlog in YSQL Aug 5, 2020
@frozenspider
Copy link
Contributor

frozenspider commented Sep 10, 2020

We should make sure we also properly support CREATE CONSTRAINT TRIGGER, including a case similar to this:

CREATE TABLE triggered(i int);

CREATE OR REPLACE FUNCTION my_trigger_func() RETURNS trigger AS $$
BEGIN
  RAISE NOTICE 'Row: % -> %', OLD.i, NEW.i;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE CONSTRAINT TRIGGER trig_after_insert AFTER INSERT ON triggered DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION my_trigger_func();

BEGIN;
SET CONSTRAINTS trig_after_insert DEFERRED;
COPY triggered FROM '/path/to/my/file' WITH (FORMAT CSV);
COMMIT;

@EmiPhil
Copy link

EmiPhil commented Feb 23, 2021

CREATE CONSTRAINT TRIGGER is also helpful for creating a sequenced foreign key constraint between two bi-temporal tables. Without it, you need to ignore the trigger enforcing the constraint when updating the referenced table, even if by the end of the transaction it would have a valid state (since updating a bi-temporal table required both an UPDATE and INSERT).

There isn't exactly a short way to show the problem (TTST = transaction-time state table, VTST = valid-time state table, pov = period of validity, pop = period of presence):

-- create a new TTST period from now to infinity
create function pop_now()
returns tstzrange
as $$ select tstzrange(now() at time zone 'utc', null, '[)') $$
language sql;

-- check if the table is a ttst
create or replace function is_ttst(_tbl regclass, out result bool)
language plpgsql
as $$
begin
    select exists (
        select from pg_attribute
        where attrelid = _tbl
        and attname = 'pop'
        and not attisdropped
        and attnum > 0
        and atttypid = 3910::oid -- 3910 = type for tstzrange
    ) into result;
end;
$$;

-- check if the table is a vtst
create or replace function is_vtst(_tbl regclass, out result bool)
language plpgsql
as $$
begin
    select exists (
        select * from pg_attribute
        where attrelid = _tbl
        and attname = 'pov'
        and not attisdropped
        and attnum > 0
        and atttypid = 3910::oid -- 3910 = type for tstzrange
    ) into result;
end;
$$;

-- check if the table is a ttst and a vtst
create function is_bitemporal(_tbl regclass, out result bool)
language plpgsql
as $$
begin
    select is_ttst(_tbl) and is_vtst(_tbl) into result;
end;
$$;

create or replace function pk_fk_pair(pk text, fk text, out result text[])
language plpgsql
as $$
begin
    select array [pk,fk] into result;
end;
$$;

-- check to see if the sequenced key of _tbl1 contains all of the sequenced key of _tbl2 at the current pop
create or replace function contains_all(_tbl1 regclass, _tbl2 regclass, key_pairs text[], out result bool)
language plpgsql
as $$
declare
    index int := 1; -- pg uses 1 based array indexing :shrug:
    pk_fk_pair text[];
    col_exists bool;
    joiners text;
    statement text;
begin
    -- each _tbl must be bitemporal
    if not is_bitemporal(_tbl1) then
        raise invalid_table_definition using message = format('table %s is not bitemporal', _tbl1);
    end if;
    if not is_bitemporal(_tbl2) then
        raise invalid_table_definition using message = format('table %s is not bitemporal', _tbl2);
    end if;

    -- key_pairs must be even
    if cardinality(key_pairs) % 2 <> 0 then
        raise invalid_parameter_value using message = 'key_pairs must be an even array of pk,fk';
    end if;

    joiners := '';
    statement := 'with each_pov as (select bool_or(t1.pov @> t2.pov) as ok from %s t1 join %s t2 on %s and upper(t1.pop) is null and upper(t2.pop) is null group by t2.pov) select coalesce(bool_and(each_pov.ok), (select count(*) = 0 from %s where upper(pop) is null)) from each_pov';
    -- we need to map the pk,fk pairs into join clauses for the statement
    loop
        exit when index = cardinality(key_pairs) + 1;
        select pk_fk_pair(quote_ident(key_pairs[index]), quote_ident(key_pairs[index + 1])) into pk_fk_pair;
        index := index + 2;

        -- check if pk exists
        select exists(
            select from pg_attribute
            where attrelid = _tbl1
            and attname = pk_fk_pair[1]
            and not attisdropped
            and attnum > 0
        ) into col_exists;
        if not col_exists then
            raise invalid_column_reference using message = format('pk column %s not found in table %s', pk_fk_pair[1], _tbl1);
        end if;

        -- check if fk exists
        select exists(
            select from pg_attribute
            where attrelid = _tbl2
            and attname = pk_fk_pair[2]
            and not attisdropped
            and attnum > 0
        ) into col_exists;
        if not col_exists then
            raise invalid_column_reference using message = format('fk column %s not found in table %s', pk_fk_pair[2], _tbl2);
        end if;

        -- add the joiner to the result
        joiners := joiners || format('t1.%s = t2.%s', pk_fk_pair[1], pk_fk_pair[2]);
    end loop;

    execute format(
        statement,
        _tbl1, _tbl2, joiners, _tbl2
    ) into result;
end;
$$;

create or replace function sequenced_fk() returns trigger
language plpgsql
as $$
begin
    -- tg_relid is the id of the table that caused the trigger to invoke
    -- tg_argv is a text array of the arguments given in the create trigger statement, which must be the reference table
    -- followed by primary key/foreign key pairs
    -- the argv is 0 indexed unlike other pg arrays :shrug:
    if not contains_all(tg_argv[0]::regclass, tg_relid, tg_argv[1:]) then
        raise foreign_key_violation using message = 'value violates sequenced foreign key';
    end if;
    return null;
end;
$$;

create or replace function sequenced_fk_ref() returns trigger
language plpgsql
as $$
begin
    -- this early break out is required to update and insert on a referenced table because Yugabyte does not support
    -- deferrable triggers
    if upper(new.pop) is not null and tg_op = 'UPDATE' then
        return null;
    end if;

    if not contains_all(tg_relid, tg_argv[0]::regclass, tg_argv[1:]) then
        raise foreign_key_violation using message = 'value violates sequenced foreign key';
    end if;
    return null;
end;
$$;

create table a (
    id int,
    pov tstzrange,
    pop tstzrange
);

create table b (
    id int,
    pov tstzrange,
    pop tstzrange,
    a_id int
);

create trigger b_fk_a
after insert or update on b
for each row execute procedure sequenced_fk('a', 'id', 'a_id');

create trigger b_fk_a_ref
after insert or update on a
for each row execute procedure sequenced_fk_ref('b', 'id', 'a_id');


-- a.1 is valid from March 2021 to forever
insert into a (id, pov, pop) values (1, '[2021-03-01,)', pop_now());

-- b.1 is valid from March 2021 to forever and must exist within the valid time of a.1
insert into b (id, pov, pop, a_id) values (1, '[2021-03-01,)', pop_now(), 1);

-- try to update the remaining a pov without disabling b, while following bitemporal table semantics
-- works because of our hack in the reference table trigger to allow updates that end the pop even if they break the fk.
-- ideally we would simply use a deferrable trigger here, but that isn't supported in Yugabyte.
begin;
update a set pop = pop_end(a.pop) where id = 1 and upper(a.pov) is null; -- this would fail normally because the fk has to exist at the best known time
insert into a (id, pov, pop) values (1, '[2021-02-01,)', pop_now());
commit;

The goal is to be able to remove this:

    if upper(new.pop) is not null and tg_op = 'UPDATE' then
        return null;
    end if;

from the sequenced_fk_ref() trigger function and replace it with a deferrable constraint trigger.

@m-iancu m-iancu removed their assignment Mar 14, 2022
@yugabyte-ci yugabyte-ci added the priority/medium Medium priority issue label Jun 9, 2022
@tyteen4a03
Copy link

Any updates on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/ysql Yugabyte SQL (YSQL) community/request Issues created by external users kind/enhancement This is an enhancement of an existing feature priority/medium Medium priority issue
Projects
Status: No status
YSQL
  
Backlog
Development

No branches or pull requests

9 participants