Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unique index on expressions doesn't work like I expect

Here is example of my problem: http://dbfiddle.uk/?rdbms=postgres_9.6&fiddle=ddb9cfd2da315ecf36cfffd66853f023

I use this function:

CREATE OR REPLACE FUNCTION inListExistOrNull(list jsonb) RETURNS boolean AS
$BODY$
    DECLARE
        r TEXT;
        i boolean := false;
        vcount int;
    BEGIN
        FOR r IN SELECT * FROM jsonb_array_elements($1) LOOP
            vcount := (SELECT COUNT(*)
                       FROM table_example
                       WHERE data->>'test' LIKE '%' || r || '%');

            i := vcount > 0;

            IF i = true THEN
                EXIT;
            END IF;
        END LOOP;

        IF i = true THEN
            RETURN true;
        END IF;

        RETURN NULL;
    END;
$BODY$
LANGUAGE 'plpgsql'
IMMUTABLE
RETURNS NULL ON NULL INPUT;

I create a table and indexes like this:

CREATE TABLE IF NOT EXISTS table_example(
   id bigserial primary key,
   data jsonb,
   txid bigint
);

CREATE INDEX IF NOT EXISTS table_example_txid_index
   ON table_example(txid);

CREATE UNIQUE INDEX IF NOT EXISTS unique_table_example
   ON table_example(inListExistOrNull(data->'test'));

I insert some rows:

INSERT INTO table_example (id, data, txid)
   VALUES (1, '{"test": ["https://example.com/test/123", "https://example.com/test/678"]}', 1);
INSERT INTO table_example (id, data, txid)
   VALUES (2, '{"test": ["https://example.com/test/b4b81fb221d4fa641", "https://example.com/test/624f3e10048245fb1"]}', 2);
INSERT INTO table_example (id, data, txid)
   VALUES (4, '{"test": ["https://example.com/test/ggg", "https://example.com/test/hhh"]}', 4);
INSERT INTO table_example (id, data, txid)
   VALUES (5, '{"test": ["https://example.com/test/ggg"]}', 5);

I have no idea why I can create a row with id = 5. It should be caught by the unique index but it isn't.

This works like I'd expect:

INSERT INTO table_example (id, data, txid)
   VALUES (6, '{"test": ["https://example.com/test/b4b81fb221d4fa641", "https://example.com/test/624f3e10048245fb1"]}', 6);
ERROR:  duplicate key value violates unique constraint "unique_table_example"
DETAIL:  Key (inlistexistornull(data -> 'test'::text))=(t) already exists.

SOLUTIONS

TRIGGER:

CREATE OR REPLACE FUNCTION inListExistOrNull() RETURNS TRIGGER AS
$BODY$
    DECLARE
        r TEXT;
        i boolean := false;
        vcount int;
        newData jsonb;
    BEGIN
        newData := NEW.data->'test';

        FOR r IN SELECT * FROM jsonb_array_elements(newData) LOOP
            vcount := (SELECT COUNT(*) FROM table_example WHERE data->>'test' LIKE '%' || r || '%');

            i := vcount > 0;

            IF i = true THEN
                RAISE 'Duplicate data: %', r USING ERRCODE = '23505';
            END IF;
        END LOOP;

        RETURN NEW;
    END;
$BODY$
LANGUAGE 'plpgsql'
STABLE;

CREATE TRIGGER inListExistOrNullTrigger
BEFORE INSERT OR UPDATE ON table_example
    FOR EACH ROW EXECUTE PROCEDURE inListExistOrNull();

UNIQUE INDEX:

CREATE OR REPLACE FUNCTION inListExistOrNull(list jsonb) RETURNS int AS
$BODY$
    DECLARE
        r TEXT;
        i boolean := false;
        vcount int;
    BEGIN
        FOR r IN SELECT * FROM jsonb_array_elements($1) LOOP
            vcount := (SELECT COUNT(*) FROM table_example WHERE data->>'test' LIKE '%' || r || '%');

            i := vcount > 0;

            IF i = true THEN
                RAISE 'Duplicate data: %', r USING ERRCODE = '23505';
            END IF;
        END LOOP;

        IF i = true
        THEN
            RETURN true;
        ELSE
            RETURN NULL;
        END IF;
    END;
$BODY$
LANGUAGE 'plpgsql'
IMMUTABLE
RETURNS NULL ON NULL INPUT;

CREATE UNIQUE INDEX IF NOT EXISTS unique_table_example ON table_example(inListExistOrNull(data->'test'));
like image 735
Arkadiusz Miszczyszyn Avatar asked Dec 11 '25 19:12

Arkadiusz Miszczyszyn


1 Answers

One problem is that your function is not really immutable, and you are lying to PostgreSQL when you label it as such. IMMUTABLE means that it always has to return the same result for the same arguments, no matter what data are in the database and how it is configured.

The other problem is that the index won't do what you want it to: It will complain only the second time you insert a value where the function result is TRUE. That is what you experience here.

Instead of an index, you could use an AFTER INSERT OR UPDATE trigger with a similar function (raising an exception if duplicates are encountered), which will do what you want. Then you can label the function STABLE as you should, and everything should work fine.

like image 106
Laurenz Albe Avatar answered Dec 15 '25 19:12

Laurenz Albe



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!