Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pN9GW-0002tX-JW for pgsql-hackers@arkaria.postgresql.org; Wed, 01 Feb 2023 09:18:53 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1pN9GV-0007ws-Fw for pgsql-hackers@arkaria.postgresql.org; Wed, 01 Feb 2023 09:18:51 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pN9GU-0007wi-Du for pgsql-hackers@lists.postgresql.org; Wed, 01 Feb 2023 09:18:51 +0000 Received: from out5-smtp.messagingengine.com ([66.111.4.29]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pN9GP-0001gC-GM for pgsql-hackers@lists.postgresql.org; Wed, 01 Feb 2023 09:18:50 +0000 Received: from compute2.internal (compute2.nyi.internal [10.202.2.46]) by mailout.nyi.internal (Postfix) with ESMTP id 2A02B5C010A for ; Wed, 1 Feb 2023 04:18:43 -0500 (EST) Received: from mailfrontend2 ([10.202.2.163]) by compute2.internal (MEProxy); Wed, 01 Feb 2023 04:18:43 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :date:date:feedback-id:feedback-id:from:from:in-reply-to :message-id:mime-version:reply-to:sender:subject:subject:to:to :x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm3; t=1675243123; x=1675329523; bh=jB10XHVpispQeuwh+5eW1TtqdrkU ZIC5jypekGea93I=; b=L3wEli+kEhx6t7xYPL8p+VPo+cBgclTD4XoNNb1idcDF 9maZTQjGGZWSZsjlgddbophl6cFtEj37EYWYHth2V5hEqi6H6FHDDCijN/l1o6U9 FMcp00OgGDI+O7KwXdgCJHrgmW9ulBdUM8pJAEBQbQ4w2U0dQU8/q+4tk8y3+PgZ ae/JqOS4FmF/W0AR4hbOzYv/4fnXMh8OTbnIw8RhZv6HZM3CS9f4wFcJVrLhDyou 5nTUzZUoMFmE0LYTD8OURnu3uL5g3142qiMQyuOzdYA1bvLziRRsA+u7XbtvyW+8 4oyJFZVSUwRQmQwAIT0ooA1d6/9YmTjf0QRlVvUYvQ== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvhedrudefiedgtdefucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmne cujfgurhepfffhvffukfggtggugfesthekredttddtjeenucfhrhhomheptehlvhgrrhho ucfjvghrrhgvrhgruceorghlvhhhvghrrhgvsegrlhhvhhdrnhhoqdhiphdrohhrgheqne cuggftrfgrthhtvghrnhephfejhedvgefhudefiefgtddtudetuddvgeevhfdvueefvdet gffhheffudfhgfejnecuffhomhgrihhnpegvnhhtvghrphhrihhsvggusgdrtghomhenuc evlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpegrlhhvhhgv rhhrvgesrghlvhhhrdhnohdqihhprdhorhhg X-ME-Proxy: Feedback-ID: ia2694551:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Wed, 1 Feb 2023 04:18:42 -0500 (EST) Received: by perhan.alvh.no-ip.org (Postfix, from userid 1000) id B7B6A9D; Wed, 1 Feb 2023 10:03:26 +0100 (CET) Date: Wed, 1 Feb 2023 10:03:26 +0100 From: Alvaro Herrera To: Pg Hackers Subject: transition tables and UPDATE Message-ID: <20230201090326.aolrmpoy5cbcjslq@alvherre.pgsql> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Earlier today I gave a talk about MERGE and wanted to provide an example with FOR EACH STATEMENT triggers using transition tables. However, I can't find a non-ugly way to obtain the NEW row that corresponds to each OLD row ... I had to resort to an ugly trick with OFFSET n LIMIT 1. Can anyone suggest anything better? I couldn't find any guidance in the docs. This is the example function I wrote: CREATE FUNCTION wine_audit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF (TG_OP = 'DELETE') THEN INSERT INTO wine_audit SELECT 'D', now(), row_to_json(o), NULL FROM old_table o; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO wine_audit SELECT 'I', now(), NULL, row_to_json(n) FROM new_table n; ELSIF (TG_OP = 'UPDATE') THEN DECLARE oldrec record; newrec jsonb; i integer := 0; BEGIN FOR oldrec IN SELECT * FROM old_table LOOP newrec := row_to_json(n) FROM new_table n OFFSET i LIMIT 1; i := i + 1; INSERT INTO wine_audit SELECT 'U', now(), row_to_json(oldrec), newrec; END LOOP; END; END IF; RETURN NULL; END; $$; CREATE TABLE wines (winery text, brand text, variety text, year int, bottles int); CREATE TABLE shipment (LIKE wines); CREATE TABLE wine_audit (op varchar(1), datetime timestamptz, oldrow jsonb, newrow jsonb); CREATE TRIGGER wine_update AFTER UPDATE ON wines REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION wine_audit(); -- I omit triggers on insert and update because the trigger code for those is trivial INSERT INTO wines VALUES ('Concha y Toro', 'Sunrise', 'Chardonnay', 2021, 12), ('Concha y Toro', 'Sunrise', 'Merlot', 2022, 12); INSERT INTO shipment VALUES ('Concha y Toro', 'Sunrise', 'Chardonnay', 2021, 96), ('Concha y Toro', 'Sunrise', 'Merlot', 2022, 120), ('Concha y Toro', 'Marqués de Casa y Concha', 'Carmenere', 2021, 48), ('Concha y Toro', 'Casillero del Diablo', 'Cabernet Sauvignon', 2019, 240); ALTER TABLE shipment ADD COLUMN marked timestamp with time zone; WITH unmarked_shipment AS (UPDATE shipment SET marked = now() WHERE marked IS NULL RETURNING winery, brand, variety, year, bottles) MERGE INTO wines AS w USING (SELECT winery, brand, variety, year, sum(bottles) as bottles FROM unmarked_shipment GROUP BY winery, brand, variety, year) AS s ON (w.winery, w.brand, w.variety, w.year) = (s.winery, s.brand, s.variety, s.year) WHEN MATCHED THEN UPDATE SET bottles = w.bottles + s.bottles WHEN NOT MATCHED THEN INSERT (winery, brand, variety, year, bottles) VALUES (s.winery, s.brand, s.variety, s.year, s.bottles) ; If you examine table wine_audit after pasting all of the above, you'll see this, which is correct: ─[ RECORD 1 ]──────────────────────────────────────────────────────────────────────────────────────────────────── op │ U datetime │ 2023-02-01 01:16:44.704036+01 oldrow │ {"year": 2021, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 12, "variety": "Chardonnay"} newrow │ {"year": 2021, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 108, "variety": "Chardonnay"} ─[ RECORD 2 ]──────────────────────────────────────────────────────────────────────────────────────────────────── op │ U datetime │ 2023-02-01 01:16:44.704036+01 oldrow │ {"year": 2022, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 12, "variety": "Merlot"} newrow │ {"year": 2022, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 132, "variety": "Merlot"} My question is how to obtain the same rows without the LIMIT/OFFSET line in the trigger function. Also: how can we "subtract" both JSON blobs so that the 'newrow' only contains the members that differ? I would like to have this: ─[ RECORD 1 ]──────────────────────────────────────────────────────────────────────────────────────────────────── op │ U datetime │ 2023-02-01 01:16:44.704036+01 oldrow │ {"year": 2021, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 12, "variety": "Chardonnay"} newrow │ {"bottles": 108} ─[ RECORD 2 ]──────────────────────────────────────────────────────────────────────────────────────────────────── op │ U datetime │ 2023-02-01 01:16:44.704036+01 oldrow │ {"year": 2022, "brand": "Sunrise", "winery": "Concha y Toro", "bottles": 12, "variety": "Merlot"} newrow │ {"bottles": 132} -- Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/ "La gente vulgar sólo piensa en pasar el tiempo; el que tiene talento, en aprovecharlo"