Изучить запрос внутри функции postgres - PullRequest
1 голос
/ 10 июля 2020

Я хотел бы использовать результат запроса в качестве условия того, какое действие выполнять внутри функции postgres. Вот моя попытка:

CREATE OR REPLACE FUNCTION upsert_bin_effect(operation_id integer, typ text, asset_id integer, qty double precision) RETURNS boolean AS $$
BEGIN
    existing_locked := (SELECT SUM(qty) AS qty INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND be.locked = true GROUP BY be.operation_id, be.type, be.asset_id);
    qty = qty - existing.locked.qty
    
    existing_unlocked := (SELECT * INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND (be.locked = false OR be.locked IS NULL));
    IF EXISTS(existing_unlocked)
    THEN            
        UPDATE bin_effect be SET be.qty = qty WHERE be.id = existing_unlocked.id
    ELSE
        INSERT INTO bin_effect be (be.operation_id, be."type", be.asset_id, be.qty) VALUES (operation_id, typ, asset_id, qty);
    END IF;
    
END;
$$ LANGUAGE plpgsql;

existing_locked может иметь несколько строк, я хотел бы вычесть сумму existing_locked.qty из входящего qty. Затем обновите запись, отличную от locked (т.е. в existing_unlocked), если она существует, с помощью net qty - в противном случае вставьте новую строку с net qty.

Если предположить, что существует таблица со следующими данными:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null

Следующий вызов:

upsert_bin_effect(1, 'A', 1, 100)

должен привести к:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null
1, 'A', 1, 70, null

следующий вызов:

upsert_bin_effect(1, 'A', 2, 100)

должен привести к:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null

Следующий вызов:

upsert_bin_effect(1, 'A', 3, 100)

должен привести к:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null
1, 'A', 3, 100, null

Чтобы лучше описать, как я бы хотел, чтобы эта функция работала, вот некоторый псевдокод javascript, который реализует желаемую функциональность:

// these are mock result sets, assume they were queried where operation_id, type, asset_id are equal and locked is true/falsy respectively.
const existingLocked = [];
const existingUnlocked = [];

function upsert_bin_effect(operationId, typ, assetId, qty) {
    const lockedQty = existingLocked.reduce((sum, r) => sum + r.qty, 0);
  
  // incoming qty represents the total qty. lockedQty represents qty for locked rows (rows we cannot update)
  // If there is nonzero lockedQty, we subtract it from qty because when we upsert qty
  // we need to ensure that all rows qty will sum to the incoming qty.
  qty = qty - lockedQty;
  
  // existingUnlocked should only ever contain a single row (because of the upsert logic below)
  if (!!existingUnlocked[0]) {
    // if there is an existing unlocked row, update it with the (incoming qty - any locked qty)
    existingUnlocked[0].update('qty', qty);
  }
  else {
    // otherwise insert a new row with qty = (incoming qty - any locked qty)
    db.binEffect.insert(operationId, typ, assetId, qty)
  }
}

Я новичок в программировании функций sql. Имеет ли это смысл? Если нет, как я могу выполнить sh то, что я пытаюсь сделать?

Ответы [ 2 ]

1 голос
/ 16 июля 2020

Позвольте мне просто предисловить к этому, сказав, что это может быть что-то вроде проблемы XY . Postgres имеет механизм блокировки строк в транзакции, но без их изменения. Это означает, что другие транзакции не могут их обновлять, но могут считывать их предварительно заблокированное состояние. Это известно как SELECT FOR UPDATE. Таким образом, ваш upsert_bin_effect может читать заблокированные строки, но не может их изменять.

Если вы создаете уникальный индекс для разблокированных строк, вы можете сделать это в одном запросе, используя INSERT ... ON CONFLICT UPDATE. Учитывая схему этого примера:

create table bin_effect (
    operation_id integer not null,
    typ text not null,
    asset_id integer not null,
    qty double precision not null,
    locked boolean not null
    -- don't understand why you were using null to indicated that the row was not locked
);

-- create a partial unique index that guarantees at most one unlocked row per "key"
create unique index bin_effect_unqiue_unlocked
    on bin_effect (operation_id, typ, asset_id) where not locked;

Затем с учетом таблицы, инициализированной следующей вставкой:

insert into bin_effect values
    (1, 'A', 1, 10, true),
    (1, 'A', 1, 20, true),
    (1, 'A', 2, 5, true),
    (1, 'A', 2, 15, false);

Затем следующий запрос вставит разблокированную строку или обновит разблокированную строку для данного operation_id, typ и asset_id. Затем вы можете использовать этот запрос как параметризованный запрос либо напрямую, либо как часть сохраненной функции. NB. Это необработанный запрос для operation_id = 1, typ = 'A', asset_id = 1 и нового количества 100.

-- pre-calculate the desired qty, so we only compute it once
with new_value as (
    select
        -- use coalesce for when there no matching rows in table (locked or otherwise)
        100 - coalesce(sum(qty), 0) as qty
    from bin_effect
    where 
        operation_id = 1
        and typ = 'A'
        and asset_id = 1
        and locked
)
-- try to insert a new row
insert into bin_effect (operation_id, typ, asset_id, locked, qty)
    values
        (1, 'A', 1, false, (select qty from new_value))
    -- if the insertion fails, then update the pre-existing row
    on conflict (operation_id, typ, asset_id) where not locked 
        do update set qty = (select qty from new_value) 
;

В качестве сохраненной функции:

create or replace function upsert_bin_effect(operation_id_ integer, typ_ text, asset_id_ integer, new_qty double precision)
returns double precision as $$
    with new_value as (
        select 
            new_qty - coalesce(sum(qty), 0) as qty
        from bin_effect
        where 
            operation_id = operation_id_
            and typ = typ_
            and asset_id = asset_id_
            and locked
    )
    insert into bin_effect (operation_id, typ, asset_id, locked, qty)
        values
            (operation_id_, typ_, asset_id_, false, (select qty from new_value))
        on conflict (operation_id, typ, asset_id) where not locked
            do update set qty = (select qty from new_value)
        returning qty
    ;
$$
language sql;

Пример использования и вывод:

postgres=# select upsert_bin_effect(1, 'A', 1, 100);
 upsert_bin_effect
-------------------
                70
(1 row)


postgres=# select upsert_bin_effect(1, 'A', 2, 100);
 upsert_bin_effect
-------------------
                95
(1 row)


postgres=# select upsert_bin_effect(1, 'A', 3, 100);
 upsert_bin_effect
-------------------
               100
(1 row)


postgres=# table bin_effect;
 operation_id | typ | asset_id | qty | locked
--------------+-----+----------+-----+--------
            1 | A   |        1 |  10 | t
            1 | A   |        1 |  20 | t
            1 | A   |        2 |   5 | t
            1 | A   |        1 |  70 | f
            1 | A   |        2 |  95 | f
            1 | A   |        3 | 100 | f
(6 rows)
1 голос
/ 10 июля 2020

Есть несколько проблем с этой функцией, прежде чем вы доберетесь до того, что хотите:

DECLARE existing RESULT

--There is no RESULT type and you do end with ; So:

DECLARE existing RECORD;

existing_locked не объявлен, поэтому присвоение ему не удастся. То же самое и для existing_unlocked.

qty = qty - existing.locked.qty не заканчивается на;

Я бы провел здесь некоторое время:

https://www.postgresql.org/docs/12/plpgsql-structure.html

Из вашего комментария ниже я не вижу, чтобы новые примеры соответствовали тому, что вы говорите, что хотите:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null
1, 'A', 1, 70, null

--The following call:

upsert_bin_effect(1, 'A', 2, 100)

--should result in:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null

«существующие могут иметь несколько строк, я хотел бы вычесть сумму существующих. qty из входящего количества для любых заблокированных строк. Затем обновите любую запись, которая не заблокирована, с помощью входящего количества, если разблокированная строка существует, в противном случае вставьте новую. "

Я бы подумал, что результат будет :

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 115, null

ОБНОВЛЕНИЕ

Удар в том, что я думаю, вы хотите. Очевидно, не тестировалось, но это должно быть разумной отправной точкой.

CREATE OR REPLACE FUNCTION upsert_bin_effect(operation_id integer, typ text, asset_id integer, qty double precision) RETURNS boolean AS $$
DECLARE
    existing_locked RECORD;
    existing_unlocked RECORD;
    net_qty float;
    unlocked_ct integer;

BEGIN
    existing_locked := (SELECT SUM(qty) AS qty INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND be.locked = true GROUP BY be.operation_id, be.type, be.asset_id);
    net_qty = qty - existing.locked.qty;
    
    existing_unlocked := (SELECT * INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND (be.locked = false OR be.locked IS NULL));
GET DIAGNOSTICS unlocked_ct = ROW_COUNT;
    IF EXISTS(existing_unlocked)
    THEN
        IF unlocked_ct = 1 THEN
           UPDATE bin_effect be SET be.qty = net_qty WHERE be.id = existing_unlocked.id;
        ELSEIF unlocked_ct > 1
            --Not sure if you want this to happen, included as example.
            RAISE EXCEPTION 'Too many unlocked row';
        END IF;
    ELSE
        INSERT INTO bin_effect be (be.operation_id, be."type", be.asset_id, be.qty) VALUES (operation_id, typ, asset_id, qty);
    END IF;
    
END;
$$ LANGUAGE plpgsql;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...